Skip to content

Commit

Permalink
Merge pull request #138 from cotype/feat/also-add-urls-to-joined-content
Browse files Browse the repository at this point in the history
fix(server): also add _url's to refs which are inside content refs
  • Loading branch information
mo-hh234 authored Aug 5, 2019
2 parents 033415f + b88b739 commit 250d310
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 44 deletions.
36 changes: 35 additions & 1 deletion src/content/rest/__tests__/joins.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,12 @@ describe("joins", () => {
[b.id]: {
_id: b.id,
_type: modelB.name,
refC: { _content: modelC.name, _id: c.id, _ref: "content" }
refC: {
_content: modelC.name,
_id: c.id,
_ref: "content",
_url: `/path/to/${c.data.title}`
}
}
},
C: {
Expand Down Expand Up @@ -146,4 +151,33 @@ describe("joins", () => {

await expect(resp).toStrictEqual(expectedResponse);
});

it("should join refs with urls when model is containing an urlPath", async () => {
const resp = await find(
modelA.name,
a.id,
{ join: { B: ["refC"] } },
false
);

await expect(resp).toMatchObject({
_refs: {
content: {
B: {
[b.id]: {
_id: b.id,
_type: modelB.name,
refC: {
_content: modelC.name,
_id: c.id,
_ref: "content",
_url: `/path/to/${c.data.title}`
}
}
}
},
media: {}
}
});
});
});
58 changes: 38 additions & 20 deletions src/persistence/ContentPersistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@ import getRefUrl from "../content/getRefUrl";
import convert from "../content/convert";
import { Config } from ".";
import { getDeepJoins } from "../content/rest/filterRefData";
import { ContentFormat, Data, MetaData } from "../../typings";
import {
ContentFormat,
Data,
MetaData,
Model,
ContentRefs,
Content
} from "../../typings";
import extractMatch from "../model/extractMatch";
import extractText from "../model/extractText";
import log from "../log";
Expand Down Expand Up @@ -244,34 +251,45 @@ export default class ContentPersistence implements Cotype.VersionedDataSource {
previewOpts.publishedOnly
);

// sort and and convert loaded content into type categories
const sortedContentRefs: { [key: string]: any } = {};
// sort content into type categories
const sortedContentRefs: ContentRefs = {};

contentRefs.forEach(c => {
contentRefs.forEach(({ data, ...ref }) => {
// ignore unknown content
const contentModel = this.getModel(c.type);
const contentModel = this.getModel(ref.type);
if (!contentModel) return;

if (!sortedContentRefs[c.type]) {
sortedContentRefs[c.type] = {};
if (!sortedContentRefs[ref.type]) {
sortedContentRefs[ref.type] = {};
}

// convert referenced data
const data = convert({
content: removeDeprecatedData(c.data, contentModel),
contentModel,
contentFormat,
allModels: this.models,
baseUrls: this.config.baseUrls,
previewOpts
});

sortedContentRefs[c.type][c.id] = {
...c,
sortedContentRefs[ref.type][ref.id] = {
...ref,
data
};
} as Content;
});

// convert sorted references
// we need to separate the sorting step from the converting step
// because we need the whole refs object to convert correctly (urls)
Object.values(sortedContentRefs).forEach(category =>
Object.values(category).forEach(entry => {
const contentModel = this.getModel(entry.type) as Model;
return {
...entry,
data: convert({
content: removeDeprecatedData(entry.data, contentModel),
contentModel,
contentRefs: sortedContentRefs,
contentFormat,
allModels: this.models,
baseUrls: this.config.baseUrls,
previewOpts
})
};
})
);

// assign media refs to an object with it's ids as keys
const media: Cotype.MediaRefs = {};
mediaRefs.forEach(r => {
Expand Down
75 changes: 52 additions & 23 deletions src/persistence/adapter/knex/KnexContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import getInverseReferenceFields from "../../../model/getInverseReferenceFields"
import log from "../../../log";
import visitModel from "../../../model/visitModel";
import { Migration } from "../../ContentPersistence";
import { Model } from "../../../../typings";

const ops: any = {
eq: "=",
Expand Down Expand Up @@ -111,6 +112,9 @@ const getRecursiveOrderField = (
return false;
};

const getModel = (name: string, models: Model[]) =>
models.find(m => m.name.toLowerCase() === name.toLowerCase());

export default class KnexContent implements ContentAdapter {
knex: knex;

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

const getLinkableModelNames = (checkModels: string[]) => {
const foundModels: string[] = [];
checkModels.forEach(name => {
const foundModel = getModel(name, models);
if (foundModel) foundModels.push(foundModel.name);
});

return foundModels;
};

const fetch = async (
ids: string[],
types: string[],
prevTypes: string[],
first: boolean
) => {
// TODO Factor out function (model, types): {hasRefs, hasInverseRefs}

// Only get references when needed
// since this can be a expensive db operation
let modelHasReverseReferences = false;
let modelHasReferences = false;

let hasRefs = false;
let hasInverseRefs = false;
let implicitTypes: string[] = [];
(first ? [model.name] : prevTypes).forEach(typeName => {
const typeModel = first
? model
: models.find(m => m.name.toLowerCase() === typeName.toLowerCase());

const typeModel = first ? model : getModel(typeName, models);
if (!typeModel) return;

visitModel(typeModel, (key, value) => {
visitModel(typeModel, (_, value) => {
if (!("type" in value)) return;

if (value.type === "references") {
modelHasReverseReferences = true;
hasRefs = true;

// No types means this data is only needed to populate _urls in refs
if (types.length === 0) {
implicitTypes = implicitTypes.concat(
getLinkableModelNames([value.model!])
);
}
}
if (value.type === "content") {
modelHasReferences = true;
hasInverseRefs = true;
// No types means this data is only needed to populate _urls in refs
if (types.length === 0) {
implicitTypes = implicitTypes.concat(
getLinkableModelNames(value.models || [value.model!])
);
}
}
});
});

// we don't need to load anything if a model has no refs and it is a first level fetch
// otherwise we still need to load the main data of that join
if (first && !modelHasReverseReferences && !modelHasReferences) return [];
if (first && !hasRefs && !hasInverseRefs) return [];

const refTypes = !!types.length ? types : implicitTypes;

const refs = modelHasReferences
? this.loadRefs(ids, !first && types, published)
const refs = hasInverseRefs
? this.loadRefs(ids, !first && refTypes, published)
: [];
const inverseRefs = modelHasReverseReferences
? this.loadInverseRefs(ids, !first && types, published)
const inverseRefs = hasRefs
? this.loadInverseRefs(ids, !first && refTypes, published)
: [];

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

let checkIds = id;
for (let i = 0; i < joins.length; i++) {

/**
* Go one level deeper than joins suggest in order to provide
* enough data to populate all _url fields later on
*/
for (let i = 0; i < joins.length + 1; i++) {
const join = joins[i];

const data = await fetch(
checkIds,
Object.keys(join),
Object.keys(join || {}),
Object.keys(joins[i - 1] || {}),
i === 0
);
Expand Down Expand Up @@ -745,7 +774,7 @@ export default class KnexContent implements ContentAdapter {
if (contents.length > 0) {
// TODO action delete/unpublish/schedule
const err = new ReferenceConflictError({ type: "content" });
err.refs = contents.map((c:any) => this.parseData(c));
err.refs = contents.map((c: any) => this.parseData(c));
throw err;
}
}
Expand Down Expand Up @@ -888,7 +917,7 @@ export default class KnexContent implements ContentAdapter {

return {
total,
items: items.map((i:any) => this.parseData(i))
items: items.map((i: any) => this.parseData(i))
};
}

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

return contents.map((c:any) => this.parseData(c));
return contents.map((c: any) => this.parseData(c));
}

async list(
Expand Down Expand Up @@ -1297,7 +1326,7 @@ export default class KnexContent implements ContentAdapter {
state: "applied"
});
const outstanding = migrations.filter(
m => !applied.some((a:any) => a.name === m.name)
m => !applied.some((a: any) => a.name === m.name)
);

if (!outstanding.length) {
Expand Down

0 comments on commit 250d310

Please sign in to comment.