multi-content-type-relation 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -0
- package/TODO.md +4 -0
- package/admin/src/components/Input/InputContentSuggestions.tsx +162 -0
- package/admin/src/components/Input/MainInput.tsx +135 -0
- package/admin/src/components/Input/PublicationState.tsx +28 -0
- package/admin/src/components/Input/TableItem.tsx +109 -0
- package/admin/src/components/Input/index.tsx +27 -0
- package/admin/src/components/PluginIcon/index.tsx +12 -0
- package/admin/src/helpers/content.ts +60 -0
- package/admin/src/helpers/storage.ts +32 -0
- package/admin/src/hooks/useSearchedEntries.ts +41 -0
- package/admin/src/index.tsx +140 -0
- package/admin/src/interface.ts +37 -0
- package/admin/src/pluginId.ts +5 -0
- package/admin/src/translations/en.json +1 -0
- package/admin/src/translations/fr.json +1 -0
- package/admin/src/utils/getTrad.ts +5 -0
- package/dist/server/bootstrap.js +5 -0
- package/dist/server/config/index.js +27 -0
- package/dist/server/content-types/index.js +3 -0
- package/dist/server/controllers/controller.js +92 -0
- package/dist/server/controllers/index.js +9 -0
- package/dist/server/destroy.js +5 -0
- package/dist/server/index.js +27 -0
- package/dist/server/interface.js +2 -0
- package/dist/server/middlewares/index.js +9 -0
- package/dist/server/middlewares/middleware.js +163 -0
- package/dist/server/policies/index.js +3 -0
- package/dist/server/register.js +15 -0
- package/dist/server/routes/index.js +29 -0
- package/dist/server/services/index.js +9 -0
- package/dist/server/services/service.js +8 -0
- package/dist/server/utils.js +15 -0
- package/dist/tsconfig.server.tsbuildinfo +1 -0
- package/package.json +53 -0
- package/server/bootstrap.ts +5 -0
- package/server/config/index.ts +28 -0
- package/server/content-types/index.ts +1 -0
- package/server/controllers/controller.ts +107 -0
- package/server/controllers/index.ts +5 -0
- package/server/destroy.ts +5 -0
- package/server/index.ts +23 -0
- package/server/interface.ts +50 -0
- package/server/middlewares/index.ts +5 -0
- package/server/middlewares/middleware.ts +197 -0
- package/server/policies/index.ts +1 -0
- package/server/register.ts +14 -0
- package/server/routes/index.ts +27 -0
- package/server/services/index.ts +5 -0
- package/server/services/service.ts +11 -0
- package/server/utils.ts +14 -0
- package/strapi-admin.js +3 -0
- package/strapi-server.js +3 -0
- package/tsconfig.json +20 -0
- package/tsconfig.server.json +25 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from "react"
|
|
2
|
+
|
|
3
|
+
import { MatchingContent } from "../interface"
|
|
4
|
+
import { fetchMatchingContent } from "../helpers/content"
|
|
5
|
+
|
|
6
|
+
export function useSearchedEntries(keyword: string, contentTypes: string, locale: string) {
|
|
7
|
+
const [loading, setLoading] = useState(false)
|
|
8
|
+
const [results, setResults] = useState<MatchingContent[]>([])
|
|
9
|
+
const [total, setTotal] = useState(0)
|
|
10
|
+
|
|
11
|
+
async function fetchEntries() {
|
|
12
|
+
setResults([])
|
|
13
|
+
setTotal(0)
|
|
14
|
+
|
|
15
|
+
if (loading || !keyword) return
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const { data, total } = await fetchMatchingContent(keyword, contentTypes, locale)
|
|
19
|
+
|
|
20
|
+
setResults(data)
|
|
21
|
+
setTotal(total)
|
|
22
|
+
} finally {
|
|
23
|
+
setLoading(false)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const timeout = setTimeout(() => fetchEntries(), 500)
|
|
29
|
+
|
|
30
|
+
return () => clearTimeout(timeout)
|
|
31
|
+
}, [keyword])
|
|
32
|
+
|
|
33
|
+
return useMemo(
|
|
34
|
+
() => ({
|
|
35
|
+
loading,
|
|
36
|
+
results,
|
|
37
|
+
total
|
|
38
|
+
}),
|
|
39
|
+
[results, total]
|
|
40
|
+
)
|
|
41
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import React, { ComponentType } from "react"
|
|
2
|
+
import { prefixPluginTranslations, CustomField } from "@strapi/helper-plugin"
|
|
3
|
+
|
|
4
|
+
import pluginPkg from "../../package.json"
|
|
5
|
+
|
|
6
|
+
import PluginIcon from "./components/PluginIcon"
|
|
7
|
+
import pluginId from "./pluginId"
|
|
8
|
+
import getTrad from "./utils/getTrad"
|
|
9
|
+
import { listContentTypes } from "./helpers/content"
|
|
10
|
+
import { setContentTypes } from "./helpers/storage"
|
|
11
|
+
|
|
12
|
+
const name = pluginPkg.strapi.name
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
async register(app) {
|
|
16
|
+
const contentTypes = await listContentTypes()
|
|
17
|
+
setContentTypes(contentTypes)
|
|
18
|
+
|
|
19
|
+
app.customFields.register({
|
|
20
|
+
name,
|
|
21
|
+
pluginId,
|
|
22
|
+
type: "richtext",
|
|
23
|
+
intlLabel: {
|
|
24
|
+
id: "multi-content-type-relation.text-ai.label",
|
|
25
|
+
defaultMessage: "Multi Content Type Relation"
|
|
26
|
+
},
|
|
27
|
+
intlDescription: {
|
|
28
|
+
id: "multi-content-type-relation.text-ai.description",
|
|
29
|
+
defaultMessage: "Write content types separated by commas"
|
|
30
|
+
},
|
|
31
|
+
icon: PluginIcon, // don't forget to create/import your icon component
|
|
32
|
+
components: {
|
|
33
|
+
Input: async () =>
|
|
34
|
+
import(/* webpackChunkName: "input-component" */ "./components/Input") as unknown as ComponentType
|
|
35
|
+
},
|
|
36
|
+
inputSize: {
|
|
37
|
+
default: 12,
|
|
38
|
+
isResizable: false
|
|
39
|
+
},
|
|
40
|
+
options: {
|
|
41
|
+
base: [
|
|
42
|
+
/*
|
|
43
|
+
Declare settings to be added to the "Base settings" section
|
|
44
|
+
of the field in the Content-Type Builder
|
|
45
|
+
*/
|
|
46
|
+
{
|
|
47
|
+
sectionTitle: {
|
|
48
|
+
id: "multi-content-type-relation.text-ai.length",
|
|
49
|
+
defaultMessage: "Content types"
|
|
50
|
+
},
|
|
51
|
+
items: contentTypes.map((contentType) => {
|
|
52
|
+
const value = contentType.info.singularName
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
intlLabel: {
|
|
56
|
+
id: `multi-content-type-relation.options.${contentType.uid}`,
|
|
57
|
+
defaultMessage: contentType.info.displayName
|
|
58
|
+
},
|
|
59
|
+
type: "checkbox",
|
|
60
|
+
name: `options.contentTypes.${value}`
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
advanced: [
|
|
66
|
+
{
|
|
67
|
+
sectionTitle: {
|
|
68
|
+
id: "global.settings",
|
|
69
|
+
defaultMessage: "Settings"
|
|
70
|
+
},
|
|
71
|
+
items: [
|
|
72
|
+
{
|
|
73
|
+
name: "required",
|
|
74
|
+
type: "checkbox",
|
|
75
|
+
intlLabel: {
|
|
76
|
+
id: getTrad("content-type-relation-select.options.advanced.requiredField"),
|
|
77
|
+
defaultMessage: "Required field"
|
|
78
|
+
},
|
|
79
|
+
description: {
|
|
80
|
+
id: getTrad("content-type-relation-select.options.advanced.requiredField.description"),
|
|
81
|
+
defaultMessage: "You won't be able to create an entry if this field is empty"
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "options.min",
|
|
86
|
+
type: "number",
|
|
87
|
+
intlLabel: {
|
|
88
|
+
id: getTrad("content-type-relation-select.options.advanced.minField"),
|
|
89
|
+
defaultMessage: "Minimum values"
|
|
90
|
+
},
|
|
91
|
+
description: {
|
|
92
|
+
id: getTrad("content-type-relation-select.options.advanced.minField.description"),
|
|
93
|
+
defaultMessage: "Minimum number of entries"
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "options.max",
|
|
98
|
+
type: "number",
|
|
99
|
+
intlLabel: {
|
|
100
|
+
id: getTrad("content-type-relation-select.options.advanced.maxField"),
|
|
101
|
+
defaultMessage: "Maximum values"
|
|
102
|
+
},
|
|
103
|
+
description: {
|
|
104
|
+
id: getTrad("content-type-relation-select.options.advanced.maxField.description"),
|
|
105
|
+
defaultMessage: "Maximum number of entries"
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
} as CustomField)
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
bootstrap(app: any) {},
|
|
116
|
+
|
|
117
|
+
async registerTrads(app: any) {
|
|
118
|
+
const { locales } = app
|
|
119
|
+
|
|
120
|
+
const importedTrads = await Promise.all(
|
|
121
|
+
(locales as any[]).map((locale) => {
|
|
122
|
+
return import(`./translations/${locale}.json`)
|
|
123
|
+
.then(({ default: data }) => {
|
|
124
|
+
return {
|
|
125
|
+
data: prefixPluginTranslations(data, pluginId),
|
|
126
|
+
locale
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
.catch(() => {
|
|
130
|
+
return {
|
|
131
|
+
data: {},
|
|
132
|
+
locale
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return Promise.resolve(importedTrads)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type PluginOption = {
|
|
2
|
+
options: {
|
|
3
|
+
min: number
|
|
4
|
+
max: number
|
|
5
|
+
contentTypes: string
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type MatchingContent = {
|
|
10
|
+
uid: string
|
|
11
|
+
displayName: string
|
|
12
|
+
searchableField: string
|
|
13
|
+
results: {
|
|
14
|
+
id: string
|
|
15
|
+
[key: string]: any
|
|
16
|
+
}[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type MatchingContentResponse = {
|
|
20
|
+
data: MatchingContent[]
|
|
21
|
+
total: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type SelectedEntry = {
|
|
25
|
+
displayName: string
|
|
26
|
+
searchableField: string
|
|
27
|
+
uid: string
|
|
28
|
+
item: {
|
|
29
|
+
id: string
|
|
30
|
+
[key: string]: any
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type FormattedStrapiEntry = {
|
|
35
|
+
uid: string
|
|
36
|
+
id: string
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = {
|
|
4
|
+
default: ({ env }) => {
|
|
5
|
+
return {
|
|
6
|
+
recursive: {
|
|
7
|
+
enabled: false,
|
|
8
|
+
maxDepth: 1
|
|
9
|
+
},
|
|
10
|
+
debug: false
|
|
11
|
+
};
|
|
12
|
+
},
|
|
13
|
+
validator(config) {
|
|
14
|
+
if (typeof config.recursive !== "object") {
|
|
15
|
+
throw new Error("recursive must be an object");
|
|
16
|
+
}
|
|
17
|
+
if (typeof config.recursive.enabled !== "boolean") {
|
|
18
|
+
throw new Error("recursive.enabled must be a boolean");
|
|
19
|
+
}
|
|
20
|
+
if (typeof config.recursive.maxDepth !== "number") {
|
|
21
|
+
throw new Error("recursive.maxDepth must be a number");
|
|
22
|
+
}
|
|
23
|
+
if (typeof config.debug !== "boolean") {
|
|
24
|
+
throw new Error("Debug must be a boolean");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = ({ strapi }) => ({
|
|
4
|
+
getMatchingContent(ctx) {
|
|
5
|
+
const contentTypes = strapi.contentTypes;
|
|
6
|
+
const body = ctx.request.body;
|
|
7
|
+
const requestedContentTypes = body.contentTypes;
|
|
8
|
+
const keyword = body.keyword;
|
|
9
|
+
const locale = body.locale;
|
|
10
|
+
const mapping = requestedContentTypes.reduce((accumulator, contentType) => {
|
|
11
|
+
Object.keys(contentTypes).forEach((model) => {
|
|
12
|
+
const strapiContentType = contentTypes[model];
|
|
13
|
+
if (strapiContentType.info.singularName === contentType || strapiContentType.info.pluralName === contentType) {
|
|
14
|
+
accumulator[contentType] = {
|
|
15
|
+
uid: model,
|
|
16
|
+
displayName: contentTypes[model].info.displayName,
|
|
17
|
+
searchableField: strapi
|
|
18
|
+
.plugin("multi-content-type-relation")
|
|
19
|
+
.service("service")
|
|
20
|
+
.getFirstStringFieldInContentType(contentTypes[model])
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
return accumulator;
|
|
25
|
+
}, {});
|
|
26
|
+
const promises = Object.keys(mapping).map((contentType) => {
|
|
27
|
+
const uid = mapping[contentType].uid;
|
|
28
|
+
return strapi
|
|
29
|
+
.entityService.findMany(uid, {
|
|
30
|
+
filters: {
|
|
31
|
+
[mapping[contentType].searchableField]: {
|
|
32
|
+
$containsi: keyword
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
locale
|
|
36
|
+
})
|
|
37
|
+
.then((results) => {
|
|
38
|
+
var _a;
|
|
39
|
+
let contents = Array.isArray(results) ? results : typeof results === "object" && results ? [results] : [];
|
|
40
|
+
const contentTypeDefinition = strapi.contentType(uid);
|
|
41
|
+
if ((_a = contentTypeDefinition === null || contentTypeDefinition === void 0 ? void 0 : contentTypeDefinition.options) === null || _a === void 0 ? void 0 : _a.draftAndPublish) {
|
|
42
|
+
contents = contents.filter((content) => content.publishedAt !== null);
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
uid,
|
|
46
|
+
displayName: mapping[contentType].displayName,
|
|
47
|
+
searchableField: mapping[contentType].searchableField,
|
|
48
|
+
results: contents
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
return Promise.all(promises);
|
|
53
|
+
},
|
|
54
|
+
validateRelations: async function (ctx) {
|
|
55
|
+
const contentTypes = strapi.contentTypes;
|
|
56
|
+
const body = ctx.request.body;
|
|
57
|
+
const entries = body.entries;
|
|
58
|
+
const promises = entries.map((entry) => {
|
|
59
|
+
return strapi
|
|
60
|
+
.entityService.findOne(entry.uid, entry.id, { populate: "deep" })
|
|
61
|
+
.then((result) => {
|
|
62
|
+
return {
|
|
63
|
+
uid: entry.uid,
|
|
64
|
+
result
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
const responses = await Promise.all(promises);
|
|
69
|
+
return responses
|
|
70
|
+
.map((response) => {
|
|
71
|
+
return {
|
|
72
|
+
displayName: contentTypes[response.uid].info.displayName,
|
|
73
|
+
uid: response.uid,
|
|
74
|
+
searchableField: strapi
|
|
75
|
+
.plugin("multi-content-type-relation")
|
|
76
|
+
.service("service")
|
|
77
|
+
.getFirstStringFieldInContentType(contentTypes[response.uid]),
|
|
78
|
+
item: response.result
|
|
79
|
+
};
|
|
80
|
+
})
|
|
81
|
+
.filter((entry) => entry.item);
|
|
82
|
+
},
|
|
83
|
+
listContentTypes: async function (ctx) {
|
|
84
|
+
const contentTypes = [];
|
|
85
|
+
for (const contentType of Object.values(strapi.contentTypes)) {
|
|
86
|
+
if ((contentType.kind === "collectionType" || contentType.kind === "singleType") && !contentType.plugin) {
|
|
87
|
+
contentTypes.push(contentType);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return contentTypes;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const controller_1 = __importDefault(require("./controller"));
|
|
7
|
+
exports.default = {
|
|
8
|
+
controller: controller_1.default
|
|
9
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const register_1 = __importDefault(require("./register"));
|
|
7
|
+
const bootstrap_1 = __importDefault(require("./bootstrap"));
|
|
8
|
+
const destroy_1 = __importDefault(require("./destroy"));
|
|
9
|
+
const config_1 = __importDefault(require("./config"));
|
|
10
|
+
const content_types_1 = __importDefault(require("./content-types"));
|
|
11
|
+
const controllers_1 = __importDefault(require("./controllers"));
|
|
12
|
+
const routes_1 = __importDefault(require("./routes"));
|
|
13
|
+
const middlewares_1 = __importDefault(require("./middlewares"));
|
|
14
|
+
const policies_1 = __importDefault(require("./policies"));
|
|
15
|
+
const services_1 = __importDefault(require("./services"));
|
|
16
|
+
exports.default = {
|
|
17
|
+
register: register_1.default,
|
|
18
|
+
bootstrap: bootstrap_1.default,
|
|
19
|
+
destroy: destroy_1.default,
|
|
20
|
+
config: config_1.default,
|
|
21
|
+
controllers: controllers_1.default,
|
|
22
|
+
routes: routes_1.default,
|
|
23
|
+
services: services_1.default,
|
|
24
|
+
contentTypes: content_types_1.default,
|
|
25
|
+
policies: policies_1.default,
|
|
26
|
+
middlewares: middlewares_1.default,
|
|
27
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const middleware_1 = __importDefault(require("./middleware"));
|
|
7
|
+
exports.default = {
|
|
8
|
+
middleware: middleware_1.default
|
|
9
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const utils_1 = require("../utils");
|
|
4
|
+
exports.default = async (ctx, next) => {
|
|
5
|
+
var _a, _b, _c, _d, _e;
|
|
6
|
+
await next();
|
|
7
|
+
if (!((_b = (_a = ctx === null || ctx === void 0 ? void 0 : ctx.request) === null || _a === void 0 ? void 0 : _a.url) === null || _b === void 0 ? void 0 : _b.startsWith("/api")))
|
|
8
|
+
return;
|
|
9
|
+
if (ctx.request.method !== "GET")
|
|
10
|
+
return;
|
|
11
|
+
if (!ctx.body)
|
|
12
|
+
return;
|
|
13
|
+
const configuration = (0, utils_1.getPluginConfiguration)();
|
|
14
|
+
const handler = ctx.state.route.handler;
|
|
15
|
+
const contentTypes = Object.keys(strapi.contentTypes);
|
|
16
|
+
(0, utils_1.log)(`URL: ${ctx.request.url} (${ctx.request.method})`);
|
|
17
|
+
(0, utils_1.log)(`Strapi Route: ${JSON.stringify(ctx.state.route, null, 2)}`);
|
|
18
|
+
const validHandler = contentTypes
|
|
19
|
+
.filter((contentType) => contentType.startsWith("api::"))
|
|
20
|
+
.some((contentType) => handler.includes(`${contentType}.findOne`) ||
|
|
21
|
+
handler.includes(`${contentType}.findMany`) ||
|
|
22
|
+
handler.includes(`${contentType}.find`));
|
|
23
|
+
(0, utils_1.log)(`Is valid handler: ${validHandler}`);
|
|
24
|
+
// Allow only findOne/findMany for native contentypes that have api::
|
|
25
|
+
if (!validHandler)
|
|
26
|
+
return;
|
|
27
|
+
const context = {
|
|
28
|
+
configuration,
|
|
29
|
+
publicationState: (_d = (_c = ctx.request.query) === null || _c === void 0 ? void 0 : _c["publicationState"]) !== null && _d !== void 0 ? _d : "live"
|
|
30
|
+
};
|
|
31
|
+
(0, utils_1.log)(" ----- ");
|
|
32
|
+
(0, utils_1.log)(`Context Body: ${JSON.stringify(ctx.body, null, 2)}`);
|
|
33
|
+
if (ctx.body.error || !((_e = ctx.body) === null || _e === void 0 ? void 0 : _e.data.attributes))
|
|
34
|
+
return;
|
|
35
|
+
const hydratedData = await augmentMRCT(ctx.body, 1, context);
|
|
36
|
+
ctx.body.data = hydratedData;
|
|
37
|
+
};
|
|
38
|
+
const augmentMRCT = async (strapiResponse, currentDepth, context) => {
|
|
39
|
+
if (Array.isArray(strapiResponse.data)) {
|
|
40
|
+
const promises = strapiResponse.data.map((item) => hydrateMRCT(item, currentDepth, context));
|
|
41
|
+
return await Promise.all(promises);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
return await hydrateMRCT(strapiResponse.data, currentDepth, context);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const hydrateMRCT = async (content, currentDepth, context) => {
|
|
48
|
+
const eligibleProperties = new Set();
|
|
49
|
+
const contentsToFetch = new Set();
|
|
50
|
+
const { configuration } = context;
|
|
51
|
+
const flattenedProperties = flattenObj(content.attributes, null);
|
|
52
|
+
for (const [key, value] of Object.entries(flattenedProperties)) {
|
|
53
|
+
if (typeof value !== "string" || !value.includes("MRCT"))
|
|
54
|
+
continue;
|
|
55
|
+
try {
|
|
56
|
+
const field = JSON.parse(value);
|
|
57
|
+
if (!Array.isArray(field))
|
|
58
|
+
continue;
|
|
59
|
+
for (const item of field) {
|
|
60
|
+
if (Object.keys(item).length !== 3 || (!item.uid && typeof item.uid !== "string") || !item.id)
|
|
61
|
+
continue;
|
|
62
|
+
const compositeID = `${item.uid}####${item.id}`;
|
|
63
|
+
eligibleProperties.add(key);
|
|
64
|
+
if (contentsToFetch.has(compositeID))
|
|
65
|
+
continue;
|
|
66
|
+
else
|
|
67
|
+
contentsToFetch.add(compositeID);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!contentsToFetch.size)
|
|
75
|
+
return content;
|
|
76
|
+
(0, utils_1.log)(`Depth: ${currentDepth}, Hydrating MCTR for ID ${content.id}`);
|
|
77
|
+
const promises = [];
|
|
78
|
+
for (const item of Array.from(contentsToFetch)) {
|
|
79
|
+
const [uid, id] = item.split("####");
|
|
80
|
+
const promise = strapi.entityService
|
|
81
|
+
.findOne(uid, id, { populate: "deep" })
|
|
82
|
+
.then(async (response) => {
|
|
83
|
+
if (!response)
|
|
84
|
+
return { uid, response };
|
|
85
|
+
if (configuration.recursive.enabled && currentDepth < configuration.recursive.maxDepth) {
|
|
86
|
+
// Entity service serve the content flattened, so we need to rebuild the API format for the hydrate recursion
|
|
87
|
+
const hydratedResponse = await hydrateMRCT({
|
|
88
|
+
id: response.id,
|
|
89
|
+
attributes: response
|
|
90
|
+
}, currentDepth + 1, context);
|
|
91
|
+
return {
|
|
92
|
+
uid,
|
|
93
|
+
response: {
|
|
94
|
+
id: response.id,
|
|
95
|
+
attributes: hydratedResponse.attributes
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
return {
|
|
101
|
+
uid,
|
|
102
|
+
response: {
|
|
103
|
+
id: response.id,
|
|
104
|
+
attributes: response
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
promises.push(promise);
|
|
110
|
+
}
|
|
111
|
+
const linkedEntries = await Promise.all(promises);
|
|
112
|
+
const filteredLinkedEntries = linkedEntries
|
|
113
|
+
.filter((linkedEntry) => Boolean(linkedEntry.response))
|
|
114
|
+
.filter((linkedEntry) => {
|
|
115
|
+
var _a, _b;
|
|
116
|
+
const contentTypeConfiguration = strapi.contentTypes[linkedEntry.uid];
|
|
117
|
+
if (!contentTypeConfiguration)
|
|
118
|
+
return true;
|
|
119
|
+
if (!((_a = contentTypeConfiguration.options) === null || _a === void 0 ? void 0 : _a.draftAndPublish))
|
|
120
|
+
return true;
|
|
121
|
+
if (context.publicationState === "preview")
|
|
122
|
+
return true;
|
|
123
|
+
return typeof ((_b = linkedEntry.response) === null || _b === void 0 ? void 0 : _b.attributes.publishedAt) === "string";
|
|
124
|
+
});
|
|
125
|
+
for (const key of Array.from(eligibleProperties)) {
|
|
126
|
+
const hydratedArray = [];
|
|
127
|
+
const unhydratedField = JSON.parse(flattenedProperties[key]);
|
|
128
|
+
for (const item of unhydratedField) {
|
|
129
|
+
const matchingContent = filteredLinkedEntries.find((linkedEntry) => item.uid === linkedEntry.uid && item.id === linkedEntry.response.id);
|
|
130
|
+
if (matchingContent) {
|
|
131
|
+
hydratedArray.push(matchingContent.response);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
flattenedProperties[key] = hydratedArray;
|
|
135
|
+
}
|
|
136
|
+
const newContent = unflatten(flattenedProperties);
|
|
137
|
+
return {
|
|
138
|
+
...content,
|
|
139
|
+
attributes: newContent
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
const flattenObj = (obj, parent, res = {}) => {
|
|
143
|
+
for (let key in obj) {
|
|
144
|
+
let propName = parent ? parent + "." + key : key;
|
|
145
|
+
if (typeof obj[key] == "object") {
|
|
146
|
+
flattenObj(obj[key], propName, res);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
res[propName] = obj[key];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return res;
|
|
153
|
+
};
|
|
154
|
+
const unflatten = (data) => {
|
|
155
|
+
var result = {};
|
|
156
|
+
for (var i in data) {
|
|
157
|
+
var keys = i.split(".");
|
|
158
|
+
keys.reduce(function (r, e, j) {
|
|
159
|
+
return r[e] || (r[e] = isNaN(Number(keys[j + 1])) ? (keys.length - 1 == j ? data[i] : {}) : []);
|
|
160
|
+
}, result);
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
163
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const middlewares_1 = __importDefault(require("./middlewares"));
|
|
7
|
+
exports.default = ({ strapi }) => {
|
|
8
|
+
// register phase
|
|
9
|
+
strapi.customFields.register({
|
|
10
|
+
name: "multi-content-type-relation",
|
|
11
|
+
plugin: "multi-content-type-relation",
|
|
12
|
+
type: "richtext"
|
|
13
|
+
});
|
|
14
|
+
strapi.server.use(middlewares_1.default.middleware);
|
|
15
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = [
|
|
4
|
+
{
|
|
5
|
+
method: "GET",
|
|
6
|
+
path: "/list-content-types",
|
|
7
|
+
handler: "controller.listContentTypes",
|
|
8
|
+
config: {
|
|
9
|
+
policies: [],
|
|
10
|
+
auth: false
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
method: "POST",
|
|
15
|
+
path: "/get-content",
|
|
16
|
+
handler: "controller.getMatchingContent",
|
|
17
|
+
config: {
|
|
18
|
+
policies: []
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
method: "POST",
|
|
23
|
+
path: "/validate-relations",
|
|
24
|
+
handler: "controller.validateRelations",
|
|
25
|
+
config: {
|
|
26
|
+
policies: []
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const service_1 = __importDefault(require("./service"));
|
|
7
|
+
exports.default = {
|
|
8
|
+
service: service_1.default
|
|
9
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = ({ strapi }) => ({
|
|
4
|
+
getFirstStringFieldInContentType(contentType) {
|
|
5
|
+
const result = Object.keys(contentType.attributes).find((attribute) => contentType.attributes[attribute].type === "string");
|
|
6
|
+
return result;
|
|
7
|
+
}
|
|
8
|
+
});
|