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,197 @@
|
|
|
1
|
+
import type { Common } from "@strapi/strapi"
|
|
2
|
+
import { getPluginConfiguration, log } from "../utils"
|
|
3
|
+
import type { Context, StrapiResponse, AnyEntity } from "../interface"
|
|
4
|
+
|
|
5
|
+
export default async (ctx, next) => {
|
|
6
|
+
await next()
|
|
7
|
+
|
|
8
|
+
if (!ctx?.request?.url?.startsWith("/api")) return
|
|
9
|
+
if (ctx.request.method !== "GET") return
|
|
10
|
+
if (!ctx.body) return
|
|
11
|
+
|
|
12
|
+
const configuration = getPluginConfiguration()
|
|
13
|
+
|
|
14
|
+
const handler = ctx.state.route.handler
|
|
15
|
+
const contentTypes = Object.keys(strapi.contentTypes)
|
|
16
|
+
|
|
17
|
+
log(`URL: ${ctx.request.url} (${ctx.request.method})`)
|
|
18
|
+
log(`Strapi Route: ${JSON.stringify(ctx.state.route, null, 2)}`)
|
|
19
|
+
|
|
20
|
+
const validHandler = contentTypes
|
|
21
|
+
.filter((contentType) => contentType.startsWith("api::"))
|
|
22
|
+
.some(
|
|
23
|
+
(contentType) =>
|
|
24
|
+
handler.includes(`${contentType}.findOne`) ||
|
|
25
|
+
handler.includes(`${contentType}.findMany`) ||
|
|
26
|
+
handler.includes(`${contentType}.find`)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
log(`Is valid handler: ${validHandler}`)
|
|
30
|
+
|
|
31
|
+
// Allow only findOne/findMany for native contentypes that have api::
|
|
32
|
+
if (!validHandler) return
|
|
33
|
+
|
|
34
|
+
const context = {
|
|
35
|
+
configuration,
|
|
36
|
+
publicationState: ctx.request.query?.["publicationState"] ?? "live"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
log(" ----- ")
|
|
40
|
+
log(`Context Body: ${JSON.stringify(ctx.body, null, 2)}`)
|
|
41
|
+
if (ctx.body.error || !ctx.body?.data.attributes) return
|
|
42
|
+
|
|
43
|
+
const hydratedData = await augmentMRCT(ctx.body, 1, context)
|
|
44
|
+
|
|
45
|
+
ctx.body.data = hydratedData
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const augmentMRCT = async (
|
|
49
|
+
strapiResponse: StrapiResponse,
|
|
50
|
+
currentDepth: number,
|
|
51
|
+
context: Context
|
|
52
|
+
): Promise<AnyEntity | AnyEntity[]> => {
|
|
53
|
+
if (Array.isArray(strapiResponse.data)) {
|
|
54
|
+
const promises = strapiResponse.data.map((item) => hydrateMRCT(item, currentDepth, context))
|
|
55
|
+
|
|
56
|
+
return await Promise.all(promises)
|
|
57
|
+
} else {
|
|
58
|
+
return await hydrateMRCT(strapiResponse.data, currentDepth, context)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const hydrateMRCT = async (content: AnyEntity, currentDepth: number, context: Context) => {
|
|
63
|
+
const eligibleProperties: Set<string> = new Set()
|
|
64
|
+
const contentsToFetch: Set<string> = new Set()
|
|
65
|
+
|
|
66
|
+
const { configuration } = context
|
|
67
|
+
|
|
68
|
+
const flattenedProperties = flattenObj(content.attributes, null)
|
|
69
|
+
|
|
70
|
+
for (const [key, value] of Object.entries(flattenedProperties)) {
|
|
71
|
+
if (typeof value !== "string" || !value.includes("MRCT")) continue
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const field = JSON.parse(value)
|
|
75
|
+
|
|
76
|
+
if (!Array.isArray(field)) continue
|
|
77
|
+
|
|
78
|
+
for (const item of field) {
|
|
79
|
+
if (Object.keys(item).length !== 3 || (!item.uid && typeof item.uid !== "string") || !item.id) continue
|
|
80
|
+
|
|
81
|
+
const compositeID = `${item.uid}####${item.id}`
|
|
82
|
+
|
|
83
|
+
eligibleProperties.add(key)
|
|
84
|
+
|
|
85
|
+
if (contentsToFetch.has(compositeID)) continue
|
|
86
|
+
else contentsToFetch.add(compositeID)
|
|
87
|
+
}
|
|
88
|
+
} catch (e) {
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!contentsToFetch.size) return content
|
|
94
|
+
|
|
95
|
+
log(`Depth: ${currentDepth}, Hydrating MCTR for ID ${content.id}`)
|
|
96
|
+
|
|
97
|
+
const promises: Promise<any>[] = []
|
|
98
|
+
for (const item of Array.from(contentsToFetch)) {
|
|
99
|
+
const [uid, id] = item.split("####")
|
|
100
|
+
const promise = strapi.entityService
|
|
101
|
+
.findOne(uid as Common.UID.ContentType, id, { populate: "deep" })
|
|
102
|
+
.then(async (response) => {
|
|
103
|
+
if (!response) return { uid, response }
|
|
104
|
+
|
|
105
|
+
if (configuration.recursive.enabled && currentDepth < configuration.recursive.maxDepth) {
|
|
106
|
+
// Entity service serve the content flattened, so we need to rebuild the API format for the hydrate recursion
|
|
107
|
+
const hydratedResponse = await hydrateMRCT(
|
|
108
|
+
{
|
|
109
|
+
id: response.id,
|
|
110
|
+
attributes: response
|
|
111
|
+
},
|
|
112
|
+
currentDepth + 1,
|
|
113
|
+
context
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
uid,
|
|
118
|
+
response: {
|
|
119
|
+
id: response.id,
|
|
120
|
+
attributes: hydratedResponse.attributes
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
return {
|
|
125
|
+
uid,
|
|
126
|
+
response: {
|
|
127
|
+
id: response.id,
|
|
128
|
+
attributes: response
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
promises.push(promise)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const linkedEntries: any[] = await Promise.all(promises)
|
|
138
|
+
|
|
139
|
+
const filteredLinkedEntries: { uid: string; response: AnyEntity }[] = linkedEntries
|
|
140
|
+
.filter((linkedEntry) => Boolean(linkedEntry.response))
|
|
141
|
+
.filter((linkedEntry) => {
|
|
142
|
+
const contentTypeConfiguration = strapi.contentTypes[linkedEntry.uid]
|
|
143
|
+
|
|
144
|
+
if (!contentTypeConfiguration) return true
|
|
145
|
+
if (!contentTypeConfiguration.options?.draftAndPublish) return true
|
|
146
|
+
if (context.publicationState === "preview") return true
|
|
147
|
+
|
|
148
|
+
return typeof linkedEntry.response?.attributes.publishedAt === "string"
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
for (const key of Array.from(eligibleProperties)) {
|
|
152
|
+
const hydratedArray: AnyEntity[] = []
|
|
153
|
+
|
|
154
|
+
const unhydratedField = JSON.parse(flattenedProperties[key]) as { uid: string; id: string }[]
|
|
155
|
+
|
|
156
|
+
for (const item of unhydratedField) {
|
|
157
|
+
const matchingContent = filteredLinkedEntries.find(
|
|
158
|
+
(linkedEntry) => item.uid === linkedEntry.uid && item.id === linkedEntry.response.id
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if (matchingContent) {
|
|
162
|
+
hydratedArray.push(matchingContent.response)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
flattenedProperties[key] = hydratedArray
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const newContent = unflatten(flattenedProperties)
|
|
170
|
+
return {
|
|
171
|
+
...content,
|
|
172
|
+
attributes: newContent
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const flattenObj = (obj: any, parent: any, res: Record<string, any> = {}) => {
|
|
177
|
+
for (let key in obj) {
|
|
178
|
+
let propName = parent ? parent + "." + key : key
|
|
179
|
+
if (typeof obj[key] == "object") {
|
|
180
|
+
flattenObj(obj[key], propName, res)
|
|
181
|
+
} else {
|
|
182
|
+
res[propName] = obj[key]
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return res
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const unflatten = (data: any) => {
|
|
189
|
+
var result = {}
|
|
190
|
+
for (var i in data) {
|
|
191
|
+
var keys = i.split(".")
|
|
192
|
+
keys.reduce(function (r: any, e, j) {
|
|
193
|
+
return r[e] || (r[e] = isNaN(Number(keys[j + 1])) ? (keys.length - 1 == j ? data[i] : {}) : [])
|
|
194
|
+
}, result)
|
|
195
|
+
}
|
|
196
|
+
return result
|
|
197
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Strapi } from "@strapi/strapi"
|
|
2
|
+
|
|
3
|
+
import middlewares from "./middlewares"
|
|
4
|
+
|
|
5
|
+
export default ({ strapi }: { strapi: Strapi }) => {
|
|
6
|
+
// register phase
|
|
7
|
+
strapi.customFields.register({
|
|
8
|
+
name: "multi-content-type-relation",
|
|
9
|
+
plugin: "multi-content-type-relation",
|
|
10
|
+
type: "richtext"
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
strapi.server.use(middlewares.middleware)
|
|
14
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export default [
|
|
2
|
+
{
|
|
3
|
+
method: "GET",
|
|
4
|
+
path: "/list-content-types",
|
|
5
|
+
handler: "controller.listContentTypes",
|
|
6
|
+
config: {
|
|
7
|
+
policies: [],
|
|
8
|
+
auth: false
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
method: "POST",
|
|
13
|
+
path: "/get-content",
|
|
14
|
+
handler: "controller.getMatchingContent",
|
|
15
|
+
config: {
|
|
16
|
+
policies: []
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
method: "POST",
|
|
21
|
+
path: "/validate-relations",
|
|
22
|
+
handler: "controller.validateRelations",
|
|
23
|
+
config: {
|
|
24
|
+
policies: []
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Strapi } from "@strapi/strapi"
|
|
2
|
+
|
|
3
|
+
export default ({ strapi }: { strapi: Strapi }) => ({
|
|
4
|
+
getFirstStringFieldInContentType(contentType) {
|
|
5
|
+
const result = Object.keys(contentType.attributes).find(
|
|
6
|
+
(attribute) => contentType.attributes[attribute].type === "string"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
return result
|
|
10
|
+
}
|
|
11
|
+
})
|
package/server/utils.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Configuration } from "./interface"
|
|
2
|
+
|
|
3
|
+
export const getPluginConfiguration = (): Configuration => {
|
|
4
|
+
const pluginConfiguration = strapi.config.get("plugin.multi-content-type-relation") as Configuration
|
|
5
|
+
|
|
6
|
+
return pluginConfiguration
|
|
7
|
+
}
|
|
8
|
+
export const log = (message: string) => {
|
|
9
|
+
const { debug } = getPluginConfiguration()
|
|
10
|
+
|
|
11
|
+
if (debug) {
|
|
12
|
+
console.log(`[MCTR DEBUG] ${message}`)
|
|
13
|
+
}
|
|
14
|
+
}
|
package/strapi-admin.js
ADDED
package/strapi-server.js
ADDED
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@strapi/typescript-utils/tsconfigs/admin",
|
|
3
|
+
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"strict": true
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
"include": ["admin", "custom.d.ts"],
|
|
10
|
+
|
|
11
|
+
"exclude": [
|
|
12
|
+
"node_modules/",
|
|
13
|
+
"dist/",
|
|
14
|
+
|
|
15
|
+
// Do not include server files in the server compilation
|
|
16
|
+
"server/",
|
|
17
|
+
// Do not include test files
|
|
18
|
+
"**/*.test.ts"
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@strapi/typescript-utils/tsconfigs/server",
|
|
3
|
+
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"outDir": "dist",
|
|
6
|
+
"rootDir": "."
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
"include": [
|
|
10
|
+
// Include the root directory
|
|
11
|
+
"server",
|
|
12
|
+
// Force the JSON files in the src folder to be included
|
|
13
|
+
"server/**/*.json"
|
|
14
|
+
],
|
|
15
|
+
|
|
16
|
+
"exclude": [
|
|
17
|
+
"node_modules/",
|
|
18
|
+
"dist/",
|
|
19
|
+
|
|
20
|
+
// Do not include admin files in the server compilation
|
|
21
|
+
"admin/",
|
|
22
|
+
// Do not include test files
|
|
23
|
+
"**/*.test.ts"
|
|
24
|
+
]
|
|
25
|
+
}
|