Skip to content

Commit 250d310

Browse files
authored
Merge pull request #138 from cotype/feat/also-add-urls-to-joined-content
fix(server): also add _url's to refs which are inside content refs
2 parents 033415f + b88b739 commit 250d310

File tree

3 files changed

+125
-44
lines changed

3 files changed

+125
-44
lines changed

src/content/rest/__tests__/joins.test.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,12 @@ describe("joins", () => {
9797
[b.id]: {
9898
_id: b.id,
9999
_type: modelB.name,
100-
refC: { _content: modelC.name, _id: c.id, _ref: "content" }
100+
refC: {
101+
_content: modelC.name,
102+
_id: c.id,
103+
_ref: "content",
104+
_url: `/path/to/${c.data.title}`
105+
}
101106
}
102107
},
103108
C: {
@@ -146,4 +151,33 @@ describe("joins", () => {
146151

147152
await expect(resp).toStrictEqual(expectedResponse);
148153
});
154+
155+
it("should join refs with urls when model is containing an urlPath", async () => {
156+
const resp = await find(
157+
modelA.name,
158+
a.id,
159+
{ join: { B: ["refC"] } },
160+
false
161+
);
162+
163+
await expect(resp).toMatchObject({
164+
_refs: {
165+
content: {
166+
B: {
167+
[b.id]: {
168+
_id: b.id,
169+
_type: modelB.name,
170+
refC: {
171+
_content: modelC.name,
172+
_id: c.id,
173+
_ref: "content",
174+
_url: `/path/to/${c.data.title}`
175+
}
176+
}
177+
}
178+
},
179+
media: {}
180+
}
181+
});
182+
});
149183
});

src/persistence/ContentPersistence.ts

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,14 @@ import getRefUrl from "../content/getRefUrl";
1515
import convert from "../content/convert";
1616
import { Config } from ".";
1717
import { getDeepJoins } from "../content/rest/filterRefData";
18-
import { ContentFormat, Data, MetaData } from "../../typings";
18+
import {
19+
ContentFormat,
20+
Data,
21+
MetaData,
22+
Model,
23+
ContentRefs,
24+
Content
25+
} from "../../typings";
1926
import extractMatch from "../model/extractMatch";
2027
import extractText from "../model/extractText";
2128
import log from "../log";
@@ -244,34 +251,45 @@ export default class ContentPersistence implements Cotype.VersionedDataSource {
244251
previewOpts.publishedOnly
245252
);
246253

247-
// sort and and convert loaded content into type categories
248-
const sortedContentRefs: { [key: string]: any } = {};
254+
// sort content into type categories
255+
const sortedContentRefs: ContentRefs = {};
249256

250-
contentRefs.forEach(c => {
257+
contentRefs.forEach(({ data, ...ref }) => {
251258
// ignore unknown content
252-
const contentModel = this.getModel(c.type);
259+
const contentModel = this.getModel(ref.type);
253260
if (!contentModel) return;
254261

255-
if (!sortedContentRefs[c.type]) {
256-
sortedContentRefs[c.type] = {};
262+
if (!sortedContentRefs[ref.type]) {
263+
sortedContentRefs[ref.type] = {};
257264
}
258265

259-
// convert referenced data
260-
const data = convert({
261-
content: removeDeprecatedData(c.data, contentModel),
262-
contentModel,
263-
contentFormat,
264-
allModels: this.models,
265-
baseUrls: this.config.baseUrls,
266-
previewOpts
267-
});
268-
269-
sortedContentRefs[c.type][c.id] = {
270-
...c,
266+
sortedContentRefs[ref.type][ref.id] = {
267+
...ref,
271268
data
272-
};
269+
} as Content;
273270
});
274271

272+
// convert sorted references
273+
// we need to separate the sorting step from the converting step
274+
// because we need the whole refs object to convert correctly (urls)
275+
Object.values(sortedContentRefs).forEach(category =>
276+
Object.values(category).forEach(entry => {
277+
const contentModel = this.getModel(entry.type) as Model;
278+
return {
279+
...entry,
280+
data: convert({
281+
content: removeDeprecatedData(entry.data, contentModel),
282+
contentModel,
283+
contentRefs: sortedContentRefs,
284+
contentFormat,
285+
allModels: this.models,
286+
baseUrls: this.config.baseUrls,
287+
previewOpts
288+
})
289+
};
290+
})
291+
);
292+
275293
// assign media refs to an object with it's ids as keys
276294
const media: Cotype.MediaRefs = {};
277295
mediaRefs.forEach(r => {

src/persistence/adapter/knex/KnexContent.ts

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import getInverseReferenceFields from "../../../model/getInverseReferenceFields"
1818
import log from "../../../log";
1919
import visitModel from "../../../model/visitModel";
2020
import { Migration } from "../../ContentPersistence";
21+
import { Model } from "../../../../typings";
2122

2223
const ops: any = {
2324
eq: "=",
@@ -111,6 +112,9 @@ const getRecursiveOrderField = (
111112
return false;
112113
};
113114

115+
const getModel = (name: string, models: Model[]) =>
116+
models.find(m => m.name.toLowerCase() === name.toLowerCase());
117+
114118
export default class KnexContent implements ContentAdapter {
115119
knex: knex;
116120

@@ -436,47 +440,67 @@ export default class KnexContent implements ContentAdapter {
436440
) {
437441
let fullData: Cotype.Data[] = [];
438442

443+
const getLinkableModelNames = (checkModels: string[]) => {
444+
const foundModels: string[] = [];
445+
checkModels.forEach(name => {
446+
const foundModel = getModel(name, models);
447+
if (foundModel) foundModels.push(foundModel.name);
448+
});
449+
450+
return foundModels;
451+
};
452+
439453
const fetch = async (
440454
ids: string[],
441455
types: string[],
442456
prevTypes: string[],
443457
first: boolean
444458
) => {
445-
// TODO Factor out function (model, types): {hasRefs, hasInverseRefs}
446-
447459
// Only get references when needed
448460
// since this can be a expensive db operation
449-
let modelHasReverseReferences = false;
450-
let modelHasReferences = false;
451-
461+
let hasRefs = false;
462+
let hasInverseRefs = false;
463+
let implicitTypes: string[] = [];
452464
(first ? [model.name] : prevTypes).forEach(typeName => {
453-
const typeModel = first
454-
? model
455-
: models.find(m => m.name.toLowerCase() === typeName.toLowerCase());
456-
465+
const typeModel = first ? model : getModel(typeName, models);
457466
if (!typeModel) return;
458467

459-
visitModel(typeModel, (key, value) => {
468+
visitModel(typeModel, (_, value) => {
460469
if (!("type" in value)) return;
461470

462471
if (value.type === "references") {
463-
modelHasReverseReferences = true;
472+
hasRefs = true;
473+
474+
// No types means this data is only needed to populate _urls in refs
475+
if (types.length === 0) {
476+
implicitTypes = implicitTypes.concat(
477+
getLinkableModelNames([value.model!])
478+
);
479+
}
464480
}
465481
if (value.type === "content") {
466-
modelHasReferences = true;
482+
hasInverseRefs = true;
483+
// No types means this data is only needed to populate _urls in refs
484+
if (types.length === 0) {
485+
implicitTypes = implicitTypes.concat(
486+
getLinkableModelNames(value.models || [value.model!])
487+
);
488+
}
467489
}
468490
});
469491
});
470492

471493
// we don't need to load anything if a model has no refs and it is a first level fetch
472494
// otherwise we still need to load the main data of that join
473-
if (first && !modelHasReverseReferences && !modelHasReferences) return [];
495+
if (first && !hasRefs && !hasInverseRefs) return [];
496+
497+
const refTypes = !!types.length ? types : implicitTypes;
474498

475-
const refs = modelHasReferences
476-
? this.loadRefs(ids, !first && types, published)
499+
const refs = hasInverseRefs
500+
? this.loadRefs(ids, !first && refTypes, published)
477501
: [];
478-
const inverseRefs = modelHasReverseReferences
479-
? this.loadInverseRefs(ids, !first && types, published)
502+
const inverseRefs = hasRefs
503+
? this.loadInverseRefs(ids, !first && refTypes, published)
480504
: [];
481505

482506
const [data, inverseData] = await Promise.all([refs, inverseRefs]);
@@ -486,12 +510,17 @@ export default class KnexContent implements ContentAdapter {
486510
};
487511

488512
let checkIds = id;
489-
for (let i = 0; i < joins.length; i++) {
513+
514+
/**
515+
* Go one level deeper than joins suggest in order to provide
516+
* enough data to populate all _url fields later on
517+
*/
518+
for (let i = 0; i < joins.length + 1; i++) {
490519
const join = joins[i];
491520

492521
const data = await fetch(
493522
checkIds,
494-
Object.keys(join),
523+
Object.keys(join || {}),
495524
Object.keys(joins[i - 1] || {}),
496525
i === 0
497526
);
@@ -745,7 +774,7 @@ export default class KnexContent implements ContentAdapter {
745774
if (contents.length > 0) {
746775
// TODO action delete/unpublish/schedule
747776
const err = new ReferenceConflictError({ type: "content" });
748-
err.refs = contents.map((c:any) => this.parseData(c));
777+
err.refs = contents.map((c: any) => this.parseData(c));
749778
throw err;
750779
}
751780
}
@@ -888,7 +917,7 @@ export default class KnexContent implements ContentAdapter {
888917

889918
return {
890919
total,
891-
items: items.map((i:any) => this.parseData(i))
920+
items: items.map((i: any) => this.parseData(i))
892921
};
893922
}
894923

@@ -910,7 +939,7 @@ export default class KnexContent implements ContentAdapter {
910939
.where("content_references.media", "=", media)
911940
.andWhere("contents.deleted", false);
912941

913-
return contents.map((c:any) => this.parseData(c));
942+
return contents.map((c: any) => this.parseData(c));
914943
}
915944

916945
async list(
@@ -1297,7 +1326,7 @@ export default class KnexContent implements ContentAdapter {
12971326
state: "applied"
12981327
});
12991328
const outstanding = migrations.filter(
1300-
m => !applied.some((a:any) => a.name === m.name)
1329+
m => !applied.some((a: any) => a.name === m.name)
13011330
);
13021331

13031332
if (!outstanding.length) {

0 commit comments

Comments
 (0)