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.
Files changed (55) hide show
  1. package/README.md +86 -0
  2. package/TODO.md +4 -0
  3. package/admin/src/components/Input/InputContentSuggestions.tsx +162 -0
  4. package/admin/src/components/Input/MainInput.tsx +135 -0
  5. package/admin/src/components/Input/PublicationState.tsx +28 -0
  6. package/admin/src/components/Input/TableItem.tsx +109 -0
  7. package/admin/src/components/Input/index.tsx +27 -0
  8. package/admin/src/components/PluginIcon/index.tsx +12 -0
  9. package/admin/src/helpers/content.ts +60 -0
  10. package/admin/src/helpers/storage.ts +32 -0
  11. package/admin/src/hooks/useSearchedEntries.ts +41 -0
  12. package/admin/src/index.tsx +140 -0
  13. package/admin/src/interface.ts +37 -0
  14. package/admin/src/pluginId.ts +5 -0
  15. package/admin/src/translations/en.json +1 -0
  16. package/admin/src/translations/fr.json +1 -0
  17. package/admin/src/utils/getTrad.ts +5 -0
  18. package/dist/server/bootstrap.js +5 -0
  19. package/dist/server/config/index.js +27 -0
  20. package/dist/server/content-types/index.js +3 -0
  21. package/dist/server/controllers/controller.js +92 -0
  22. package/dist/server/controllers/index.js +9 -0
  23. package/dist/server/destroy.js +5 -0
  24. package/dist/server/index.js +27 -0
  25. package/dist/server/interface.js +2 -0
  26. package/dist/server/middlewares/index.js +9 -0
  27. package/dist/server/middlewares/middleware.js +163 -0
  28. package/dist/server/policies/index.js +3 -0
  29. package/dist/server/register.js +15 -0
  30. package/dist/server/routes/index.js +29 -0
  31. package/dist/server/services/index.js +9 -0
  32. package/dist/server/services/service.js +8 -0
  33. package/dist/server/utils.js +15 -0
  34. package/dist/tsconfig.server.tsbuildinfo +1 -0
  35. package/package.json +53 -0
  36. package/server/bootstrap.ts +5 -0
  37. package/server/config/index.ts +28 -0
  38. package/server/content-types/index.ts +1 -0
  39. package/server/controllers/controller.ts +107 -0
  40. package/server/controllers/index.ts +5 -0
  41. package/server/destroy.ts +5 -0
  42. package/server/index.ts +23 -0
  43. package/server/interface.ts +50 -0
  44. package/server/middlewares/index.ts +5 -0
  45. package/server/middlewares/middleware.ts +197 -0
  46. package/server/policies/index.ts +1 -0
  47. package/server/register.ts +14 -0
  48. package/server/routes/index.ts +27 -0
  49. package/server/services/index.ts +5 -0
  50. package/server/services/service.ts +11 -0
  51. package/server/utils.ts +14 -0
  52. package/strapi-admin.js +3 -0
  53. package/strapi-server.js +3 -0
  54. package/tsconfig.json +20 -0
  55. 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,5 @@
1
+ import service from "./service"
2
+
3
+ export default {
4
+ service
5
+ }
@@ -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
+ })
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+
3
+ module.exports = require('./admin/src').default;
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+
3
+ module.exports = require('./dist/server');
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
+ }