strapi-relation-names 0.0.0 → 1.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 CHANGED
@@ -9,6 +9,10 @@ Display Strapi relations using readable labels built from your content fields in
9
9
  ![License: MIT](https://img.shields.io/badge/license-MIT-green)
10
10
  ![npm](https://img.shields.io/npm/dt/strapi-relation-names)
11
11
 
12
+ Ever wanted to display a relation as custom text or with nested field values? This plugin solves exactly that:
13
+
14
+ ![Relation labels example](assets/example.png)
15
+
12
16
  ---
13
17
 
14
18
  ## ✨ Features
@@ -92,14 +96,30 @@ After saving the settings, the Strapi Admin reloads automatically so open Conten
92
96
 
93
97
  ---
94
98
 
95
- ## ⚙️ Templates
99
+ ## ⚙️ Configuration
96
100
 
97
- Templates use direct field placeholders:
101
+ Possible configuration keys are listed below; omitted keys keep the plugin defaults.
98
102
 
99
- ```text
100
- {fieldName}
103
+ | Key | Description | Possible values |
104
+ | ------------- | ----------------------------------------------------------------------------------------------- | --------------- |
105
+ | `collections` | Limit relation names to the listed collection and single type UIDs. Leave empty to include all. | `string[]` |
106
+
107
+ Example:
108
+
109
+ ```javascript
110
+ // config/plugins.{js,ts}
111
+ 'strapi-relation-names': {
112
+ enabled: true,
113
+ config: {
114
+ collections: ['api::article.article', 'api::author.author'],
115
+ },
116
+ },
101
117
  ```
102
118
 
119
+ ---
120
+
121
+ ## ⚙️ Templates
122
+
103
123
  Multiple placeholders can be combined:
104
124
 
105
125
  ```text
@@ -122,13 +142,7 @@ Customer: {firstName} {lastName}
122
142
 
123
143
  ## 🗂️ How It Works
124
144
 
125
- The plugin changes how relations are displayed inside the Strapi Admin panel.
126
-
127
- The first placeholder in a template is used internally by Strapi's Admin relation renderer.
128
-
129
- Strapi's existing relation search and sorting continue to use the configured default main field.
130
-
131
- Missing field values are replaced with an empty string.
145
+ At runtime, the server-side plugin decorates Strapi Content Manager relation responses and field metadata. It reads the configured template, validates each placeholder against the related content type schema, and renders the label from the returned relation data. If a value is not present, it performs a permission-aware hydration query; nested component paths are loaded by populating their component root. The rendered label is then exposed through Strapi's normal relation display mechanism. Missing field values are replaced with an empty string.
132
146
 
133
147
  For example:
134
148
 
@@ -156,9 +170,10 @@ The default label is also used when:
156
170
 
157
171
  - [ ] Live label previews while editing templates
158
172
  - [ ] Define default relation templates in plugin configuration
159
- - [ ] Support nested field paths in templates
173
+ - [x] Support nested field paths in templates
160
174
  - [ ] Import and export relation-label settings
161
175
  - [ ] Conditional labels and configurable fallback rules
176
+ - [ ] Support label transformation functions (e.g., `toUpperCase`, `toLowerCase`, `capitalize`, etc.)
162
177
 
163
178
  If you have any feature requests or suggestions, please open a dedicated issue.
164
179
 
@@ -5,7 +5,7 @@ const designSystem = require("@strapi/design-system");
5
5
  const react = require("react");
6
6
  const reactIntl = require("react-intl");
7
7
  const admin = require("@strapi/strapi/admin");
8
- const index = require("./index-SsQXjQq0.js");
8
+ const index = require("./index-CKaZcnCM.js");
9
9
  const FIELD_TYPES = /* @__PURE__ */ new Set([
10
10
  "string",
11
11
  "text",
@@ -21,6 +21,16 @@ const FIELD_TYPES = /* @__PURE__ */ new Set([
21
21
  "time",
22
22
  "boolean"
23
23
  ]);
24
+ const SYSTEM_SCALAR_FIELDS = [
25
+ "id",
26
+ "documentId",
27
+ "createdAt",
28
+ "updatedAt",
29
+ "publishedAt",
30
+ "locale",
31
+ "status"
32
+ ];
33
+ const PLACEHOLDER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/;
24
34
  const getPlaceholders = (template) => {
25
35
  if (!template.trim()) {
26
36
  return null;
@@ -33,7 +43,7 @@ const getPlaceholders = (template) => {
33
43
  return null;
34
44
  }
35
45
  const name = match[1];
36
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
46
+ if (!PLACEHOLDER_PATTERN.test(name)) {
37
47
  return null;
38
48
  }
39
49
  placeholders.push(name);
@@ -44,16 +54,60 @@ const getPlaceholders = (template) => {
44
54
  }
45
55
  return placeholders.length > 0 ? placeholders : null;
46
56
  };
47
- const validateTemplate = (template, attributes) => {
57
+ const validateTemplate = (template, attributes, schemas) => {
48
58
  const placeholders = getPlaceholders(template);
49
59
  if (!placeholders) {
50
60
  return "Use at least one valid {fieldName} placeholder.";
51
61
  }
52
- const invalidField = placeholders.find(
53
- (field) => !FIELD_TYPES.has(attributes?.[field]?.type ?? "")
54
- );
62
+ const invalidField = placeholders.find((path) => {
63
+ const segments = path.split(".");
64
+ let currentAttributes = attributes;
65
+ for (const [index2, segment] of segments.entries()) {
66
+ if (index2 === 0 && segments.length === 1 && SYSTEM_SCALAR_FIELDS.includes(segment)) {
67
+ return false;
68
+ }
69
+ const attribute = currentAttributes?.[segment];
70
+ const isLastSegment = index2 === segments.length - 1;
71
+ if (!attribute || attribute.private === true) {
72
+ return true;
73
+ }
74
+ if (isLastSegment) {
75
+ return !FIELD_TYPES.has(attribute.type ?? "");
76
+ }
77
+ if (attribute.type !== "component" || !attribute.component) {
78
+ return true;
79
+ }
80
+ currentAttributes = schemas?.get(attribute.component)?.attributes;
81
+ }
82
+ return true;
83
+ });
55
84
  return invalidField ? `“${invalidField}” is not an available scalar field.` : null;
56
85
  };
86
+ const getAvailableFields = (schema, schemas, prefix = "", ancestors = /* @__PURE__ */ new Set()) => {
87
+ if (!schema) {
88
+ return [];
89
+ }
90
+ const fields = Object.entries(schema.attributes ?? {}).flatMap(([name, attribute]) => {
91
+ if (attribute.private === true) {
92
+ return [];
93
+ }
94
+ const path = `${prefix}${name}`;
95
+ if (FIELD_TYPES.has(attribute.type ?? "")) {
96
+ return [path];
97
+ }
98
+ if (attribute.type !== "component" || !attribute.component || ancestors.has(attribute.component)) {
99
+ return [];
100
+ }
101
+ return getAvailableFields(
102
+ schemas.get(attribute.component),
103
+ schemas,
104
+ `${path}.`,
105
+ /* @__PURE__ */ new Set([...ancestors, attribute.component])
106
+ );
107
+ });
108
+ const systemFields = prefix === "" ? SYSTEM_SCALAR_FIELDS : [];
109
+ return [...systemFields, ...fields.filter((field) => !systemFields.includes(field))];
110
+ };
57
111
  const PLUGIN_SETTINGS_PATH = "/strapi-relation-names/settings";
58
112
  const getRelationTarget = (attribute) => attribute.targetModel ?? attribute.target;
59
113
  const SettingsPage = () => {
@@ -143,7 +197,7 @@ const SettingsPage = () => {
143
197
  !isLoading ? rows.map(({ source, fieldName, target }) => {
144
198
  const targetAttributes = target?.attributes ?? {};
145
199
  const template = settings.relations[source.uid]?.[fieldName] ?? "";
146
- const validationError = template ? validateTemplate(template, targetAttributes) : null;
200
+ const validationError = template ? validateTemplate(template, targetAttributes, schemaMap) : null;
147
201
  return /* @__PURE__ */ jsxRuntime.jsxs(
148
202
  designSystem.Box,
149
203
  {
@@ -186,7 +240,7 @@ const SettingsPage = () => {
186
240
  formatMessage({ id: index.getTranslation("settings.availableFields") }),
187
241
  ":",
188
242
  " ",
189
- Object.keys(targetAttributes).join(", ")
243
+ getAvailableFields(target, schemaMap).join(", ")
190
244
  ] }) })
191
245
  ]
192
246
  },
@@ -3,7 +3,7 @@ import { Main, Box, Typography, Field, TextInput, Flex, Button } from "@strapi/d
3
3
  import { useState, useCallback, useEffect, useMemo } from "react";
4
4
  import { useIntl } from "react-intl";
5
5
  import { useFetchClient } from "@strapi/strapi/admin";
6
- import { g as getTranslation } from "./index-CJO9wtUk.mjs";
6
+ import { g as getTranslation } from "./index-DGXo1dxk.mjs";
7
7
  const FIELD_TYPES = /* @__PURE__ */ new Set([
8
8
  "string",
9
9
  "text",
@@ -19,6 +19,16 @@ const FIELD_TYPES = /* @__PURE__ */ new Set([
19
19
  "time",
20
20
  "boolean"
21
21
  ]);
22
+ const SYSTEM_SCALAR_FIELDS = [
23
+ "id",
24
+ "documentId",
25
+ "createdAt",
26
+ "updatedAt",
27
+ "publishedAt",
28
+ "locale",
29
+ "status"
30
+ ];
31
+ const PLACEHOLDER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/;
22
32
  const getPlaceholders = (template) => {
23
33
  if (!template.trim()) {
24
34
  return null;
@@ -31,7 +41,7 @@ const getPlaceholders = (template) => {
31
41
  return null;
32
42
  }
33
43
  const name = match[1];
34
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
44
+ if (!PLACEHOLDER_PATTERN.test(name)) {
35
45
  return null;
36
46
  }
37
47
  placeholders.push(name);
@@ -42,16 +52,60 @@ const getPlaceholders = (template) => {
42
52
  }
43
53
  return placeholders.length > 0 ? placeholders : null;
44
54
  };
45
- const validateTemplate = (template, attributes) => {
55
+ const validateTemplate = (template, attributes, schemas) => {
46
56
  const placeholders = getPlaceholders(template);
47
57
  if (!placeholders) {
48
58
  return "Use at least one valid {fieldName} placeholder.";
49
59
  }
50
- const invalidField = placeholders.find(
51
- (field) => !FIELD_TYPES.has(attributes?.[field]?.type ?? "")
52
- );
60
+ const invalidField = placeholders.find((path) => {
61
+ const segments = path.split(".");
62
+ let currentAttributes = attributes;
63
+ for (const [index, segment] of segments.entries()) {
64
+ if (index === 0 && segments.length === 1 && SYSTEM_SCALAR_FIELDS.includes(segment)) {
65
+ return false;
66
+ }
67
+ const attribute = currentAttributes?.[segment];
68
+ const isLastSegment = index === segments.length - 1;
69
+ if (!attribute || attribute.private === true) {
70
+ return true;
71
+ }
72
+ if (isLastSegment) {
73
+ return !FIELD_TYPES.has(attribute.type ?? "");
74
+ }
75
+ if (attribute.type !== "component" || !attribute.component) {
76
+ return true;
77
+ }
78
+ currentAttributes = schemas?.get(attribute.component)?.attributes;
79
+ }
80
+ return true;
81
+ });
53
82
  return invalidField ? `“${invalidField}” is not an available scalar field.` : null;
54
83
  };
84
+ const getAvailableFields = (schema, schemas, prefix = "", ancestors = /* @__PURE__ */ new Set()) => {
85
+ if (!schema) {
86
+ return [];
87
+ }
88
+ const fields = Object.entries(schema.attributes ?? {}).flatMap(([name, attribute]) => {
89
+ if (attribute.private === true) {
90
+ return [];
91
+ }
92
+ const path = `${prefix}${name}`;
93
+ if (FIELD_TYPES.has(attribute.type ?? "")) {
94
+ return [path];
95
+ }
96
+ if (attribute.type !== "component" || !attribute.component || ancestors.has(attribute.component)) {
97
+ return [];
98
+ }
99
+ return getAvailableFields(
100
+ schemas.get(attribute.component),
101
+ schemas,
102
+ `${path}.`,
103
+ /* @__PURE__ */ new Set([...ancestors, attribute.component])
104
+ );
105
+ });
106
+ const systemFields = prefix === "" ? SYSTEM_SCALAR_FIELDS : [];
107
+ return [...systemFields, ...fields.filter((field) => !systemFields.includes(field))];
108
+ };
55
109
  const PLUGIN_SETTINGS_PATH = "/strapi-relation-names/settings";
56
110
  const getRelationTarget = (attribute) => attribute.targetModel ?? attribute.target;
57
111
  const SettingsPage = () => {
@@ -141,7 +195,7 @@ const SettingsPage = () => {
141
195
  !isLoading ? rows.map(({ source, fieldName, target }) => {
142
196
  const targetAttributes = target?.attributes ?? {};
143
197
  const template = settings.relations[source.uid]?.[fieldName] ?? "";
144
- const validationError = template ? validateTemplate(template, targetAttributes) : null;
198
+ const validationError = template ? validateTemplate(template, targetAttributes, schemaMap) : null;
145
199
  return /* @__PURE__ */ jsxs(
146
200
  Box,
147
201
  {
@@ -184,7 +238,7 @@ const SettingsPage = () => {
184
238
  formatMessage({ id: getTranslation("settings.availableFields") }),
185
239
  ":",
186
240
  " ",
187
- Object.keys(targetAttributes).join(", ")
241
+ getAvailableFields(target, schemaMap).join(", ")
188
242
  ] }) })
189
243
  ]
190
244
  },
@@ -89,7 +89,7 @@ const plugin = {
89
89
  id: `${PLUGIN_ID}.plugin.name`,
90
90
  defaultMessage: "Relation Names"
91
91
  },
92
- Component: () => Promise.resolve().then(() => require("./SettingsPage-C1nFFJjH.js")).then((module2) => ({ default: module2.SettingsPage })),
92
+ Component: () => Promise.resolve().then(() => require("./SettingsPage-CaBXOirN.js")).then((module2) => ({ default: module2.SettingsPage })),
93
93
  permissions: []
94
94
  });
95
95
  app.registerPlugin({
@@ -88,7 +88,7 @@ const plugin = {
88
88
  id: `${PLUGIN_ID}.plugin.name`,
89
89
  defaultMessage: "Relation Names"
90
90
  },
91
- Component: () => import("./SettingsPage-BQo5mpAY.mjs").then((module) => ({ default: module.SettingsPage })),
91
+ Component: () => import("./SettingsPage-CpTyZSvB.mjs").then((module) => ({ default: module.SettingsPage })),
92
92
  permissions: []
93
93
  });
94
94
  app.registerPlugin({
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
3
- const index = require("./index-SsQXjQq0.js");
3
+ const index = require("./index-CKaZcnCM.js");
4
4
  exports.default = index.plugin;
@@ -1,4 +1,4 @@
1
- import { p } from "./index-CJO9wtUk.mjs";
1
+ import { p } from "./index-DGXo1dxk.mjs";
2
2
  export {
3
3
  p as default
4
4
  };
@@ -6,6 +6,8 @@ export type SchemaAttribute = {
6
6
  type?: string;
7
7
  targetModel?: string;
8
8
  target?: string;
9
+ component?: string;
10
+ private?: boolean;
9
11
  relationType?: string;
10
12
  };
11
13
  export type Schema = {
@@ -1,5 +1,12 @@
1
- declare const getPlaceholders: (template: string) => string[] | null;
2
- declare const validateTemplate: (template: string, attributes: Record<string, {
1
+ type TemplateAttribute = {
3
2
  type?: string;
4
- }> | undefined) => string | null;
5
- export { getPlaceholders, validateTemplate };
3
+ component?: string;
4
+ private?: boolean;
5
+ };
6
+ type TemplateSchema = {
7
+ attributes?: Record<string, TemplateAttribute>;
8
+ };
9
+ declare const getPlaceholders: (template: string) => string[] | null;
10
+ declare const validateTemplate: (template: string, attributes: Record<string, TemplateAttribute> | undefined, schemas?: ReadonlyMap<string, TemplateSchema>) => string | null;
11
+ declare const getAvailableFields: (schema: TemplateSchema | undefined, schemas: ReadonlyMap<string, TemplateSchema>, prefix?: string, ancestors?: Set<string>) => string[];
12
+ export { getAvailableFields, getPlaceholders, validateTemplate };
@@ -151,7 +151,22 @@ const SCALAR_TYPES = /* @__PURE__ */ new Set([
151
151
  "time",
152
152
  "boolean"
153
153
  ]);
154
+ const SYSTEM_SCALAR_FIELDS = /* @__PURE__ */ new Set([
155
+ "id",
156
+ "documentId",
157
+ "createdAt",
158
+ "updatedAt",
159
+ "publishedAt",
160
+ "locale",
161
+ "status"
162
+ ]);
163
+ const PLACEHOLDER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/;
154
164
  const isRecord$1 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
165
+ const getPathValue = (value, path) => {
166
+ return path.split(".").reduce((current, segment) => {
167
+ return isRecord$1(current) ? current[segment] : void 0;
168
+ }, value);
169
+ };
155
170
  const parseTemplate = (template) => {
156
171
  if (!template.trim()) {
157
172
  return null;
@@ -172,7 +187,7 @@ const parseTemplate = (template) => {
172
187
  return null;
173
188
  }
174
189
  const name = template.slice(openingBrace + 1, end);
175
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
190
+ if (!PLACEHOLDER_PATTERN.test(name)) {
176
191
  return null;
177
192
  }
178
193
  placeholders.push(name);
@@ -180,17 +195,35 @@ const parseTemplate = (template) => {
180
195
  }
181
196
  return placeholders.length > 0 ? placeholders : null;
182
197
  };
183
- const compileTemplate = (template, targetSchema) => {
198
+ const compileTemplate = (template, targetSchema, resolveSchema) => {
184
199
  const placeholders = parseTemplate(template);
185
- const attributes = targetSchema?.attributes ?? {};
186
200
  if (!placeholders || !targetSchema) {
187
201
  return null;
188
202
  }
189
- const valid = placeholders.every((name) => {
190
- const attribute = attributes[name];
191
- return Boolean(
192
- attribute && attribute.private !== true && SCALAR_TYPES.has(attribute.type ?? "")
193
- );
203
+ const valid = placeholders.every((path) => {
204
+ const segments = path.split(".");
205
+ let schema = targetSchema;
206
+ for (const [index2, segment] of segments.entries()) {
207
+ const attribute = schema.attributes?.[segment];
208
+ const isLastSegment = index2 === segments.length - 1;
209
+ if (index2 === 0 && isLastSegment && SYSTEM_SCALAR_FIELDS.has(segment)) {
210
+ return true;
211
+ }
212
+ if (!attribute || attribute.private === true) {
213
+ return false;
214
+ }
215
+ if (isLastSegment) {
216
+ return SCALAR_TYPES.has(attribute.type ?? "");
217
+ }
218
+ if (attribute.type !== "component" || !attribute.component || !resolveSchema) {
219
+ return false;
220
+ }
221
+ schema = resolveSchema(attribute.component);
222
+ if (!schema) {
223
+ return false;
224
+ }
225
+ }
226
+ return false;
194
227
  });
195
228
  if (!valid) {
196
229
  return null;
@@ -198,14 +231,14 @@ const compileTemplate = (template, targetSchema) => {
198
231
  return {
199
232
  template,
200
233
  placeholders,
201
- displayField: placeholders[0]
234
+ displayField: placeholders[0].split(".").slice(-1)[0] ?? placeholders[0]
202
235
  };
203
236
  };
204
237
  const renderTemplate = (compiled, values) => {
205
238
  const rendered = compiled.template.replace(
206
- /\{([A-Za-z_][A-Za-z0-9_]*)\}/g,
239
+ /\{([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)\}/g,
207
240
  (_match, name) => {
208
- const value = values[name];
241
+ const value = getPathValue(values, name);
209
242
  return value === null || value === void 0 ? "" : String(value);
210
243
  }
211
244
  );
@@ -272,28 +305,40 @@ const getRuntime = async (strapi, settings2, sourceUid, targetField, userAbility
272
305
  }
273
306
  const template = settings2.relations[sourceUid]?.[targetField];
274
307
  const targetSchema = strapi.getModel(attribute.target);
275
- const compiled = typeof template === "string" ? compileTemplate(template, targetSchema) : null;
308
+ const compiled = typeof template === "string" ? compileTemplate(
309
+ template,
310
+ targetSchema,
311
+ (uid) => strapi.getModel(uid)
312
+ ) : null;
276
313
  if (!compiled || !targetSchema) {
277
314
  return null;
278
315
  }
316
+ const originalMainField = await getOriginalMainField(
317
+ strapi,
318
+ sourceSchema,
319
+ targetSchema,
320
+ targetField,
321
+ userAbility
322
+ );
279
323
  return {
280
324
  ...compiled,
325
+ displayField: compiled.placeholders.some((placeholder) => placeholder.includes(".")) ? originalMainField : compiled.displayField,
281
326
  sourceUid,
282
327
  targetUid: targetSchema.uid,
283
- originalMainField: await getOriginalMainField(
284
- strapi,
285
- sourceSchema,
286
- targetSchema,
287
- targetField,
288
- userAbility
289
- )
328
+ originalMainField
290
329
  };
291
330
  };
331
+ const buildPopulate = (placeholders) => {
332
+ const roots = new Set(
333
+ placeholders.filter((placeholder) => placeholder.includes(".")).map((placeholder) => placeholder.split(".")[0])
334
+ );
335
+ return roots.size > 0 ? Object.fromEntries(Array.from(roots, (root) => [root, true])) : void 0;
336
+ };
292
337
  const hydrateValues = async (strapi, ctx, runtime, values) => {
293
338
  const targetSchema = strapi.getModel(runtime.targetUid);
294
339
  const targetModelType = targetSchema.modelType;
295
340
  const missingFields = runtime.placeholders.filter(
296
- (field) => values.some((value) => !Object.prototype.hasOwnProperty.call(value, field))
341
+ (field) => values.some((value) => getPathValue(value, field) === void 0)
297
342
  );
298
343
  if (missingFields.length === 0) {
299
344
  return new Map(values.map((value) => [value, value]));
@@ -306,7 +351,7 @@ const hydrateValues = async (strapi, ctx, runtime, values) => {
306
351
  const permissionChecker = strapi.plugin("content-manager").service("permission-checker").create({ userAbility: ctx.state.userAbility, model: runtime.targetUid });
307
352
  const fields = Array.from(
308
353
  /* @__PURE__ */ new Set([
309
- ...runtime.placeholders,
354
+ ...runtime.placeholders.filter((field) => !field.includes(".")),
310
355
  runtime.originalMainField,
311
356
  "id",
312
357
  "documentId",
@@ -314,10 +359,12 @@ const hydrateValues = async (strapi, ctx, runtime, values) => {
314
359
  "publishedAt"
315
360
  ])
316
361
  );
362
+ const populate = buildPopulate(runtime.placeholders);
317
363
  const identityField = targetModelType === "component" ? "id" : "documentId";
318
364
  const permissionQuery = await permissionChecker.sanitizedQuery.read({
319
365
  fields,
320
- filters: { [identityField]: { $in: identities } }
366
+ filters: { [identityField]: { $in: identities } },
367
+ ...populate ? { populate } : {}
321
368
  });
322
369
  const query = strapi.get("query-params").transform(runtime.targetUid, permissionQuery);
323
370
  const hydrated = await strapi.db.query(runtime.targetUid).findMany(query);
@@ -370,23 +417,27 @@ const relationLabels = ({ strapi }) => {
370
417
  const targetSchema = strapi.getModel(attribute.target);
371
418
  const compiled = compileTemplate(
372
419
  settings2.relations[sourceUid]?.[fieldName] ?? "",
373
- targetSchema
420
+ targetSchema,
421
+ (uid) => strapi.getModel(uid)
374
422
  );
375
423
  if (!compiled || !isRecord$1(metadata)) {
376
424
  continue;
377
425
  }
426
+ const editMetadata = isRecord$1(metadata.edit) ? metadata.edit : {};
427
+ const configuredMainField = typeof editMetadata.mainField === "string" ? editMetadata.mainField : "id";
428
+ const displayField = compiled.placeholders.some((placeholder) => placeholder.includes(".")) ? configuredMainField : compiled.displayField;
378
429
  metadatas[fieldName] = {
379
430
  ...metadata,
380
431
  edit: {
381
- ...isRecord$1(metadata.edit) ? metadata.edit : {},
382
- mainField: compiled.displayField
432
+ ...editMetadata,
433
+ mainField: displayField
383
434
  },
384
435
  list: {
385
436
  ...isRecord$1(metadata.list) ? metadata.list : {},
386
- mainField: compiled.displayField
437
+ mainField: displayField
387
438
  }
388
439
  };
389
- relationNames[fieldName] = { mainField: compiled.displayField };
440
+ relationNames[fieldName] = { mainField: displayField };
390
441
  }
391
442
  return {
392
443
  ...configuration,
@@ -149,7 +149,22 @@ const SCALAR_TYPES = /* @__PURE__ */ new Set([
149
149
  "time",
150
150
  "boolean"
151
151
  ]);
152
+ const SYSTEM_SCALAR_FIELDS = /* @__PURE__ */ new Set([
153
+ "id",
154
+ "documentId",
155
+ "createdAt",
156
+ "updatedAt",
157
+ "publishedAt",
158
+ "locale",
159
+ "status"
160
+ ]);
161
+ const PLACEHOLDER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/;
152
162
  const isRecord$1 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
163
+ const getPathValue = (value, path) => {
164
+ return path.split(".").reduce((current, segment) => {
165
+ return isRecord$1(current) ? current[segment] : void 0;
166
+ }, value);
167
+ };
153
168
  const parseTemplate = (template) => {
154
169
  if (!template.trim()) {
155
170
  return null;
@@ -170,7 +185,7 @@ const parseTemplate = (template) => {
170
185
  return null;
171
186
  }
172
187
  const name = template.slice(openingBrace + 1, end);
173
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
188
+ if (!PLACEHOLDER_PATTERN.test(name)) {
174
189
  return null;
175
190
  }
176
191
  placeholders.push(name);
@@ -178,17 +193,35 @@ const parseTemplate = (template) => {
178
193
  }
179
194
  return placeholders.length > 0 ? placeholders : null;
180
195
  };
181
- const compileTemplate = (template, targetSchema) => {
196
+ const compileTemplate = (template, targetSchema, resolveSchema) => {
182
197
  const placeholders = parseTemplate(template);
183
- const attributes = targetSchema?.attributes ?? {};
184
198
  if (!placeholders || !targetSchema) {
185
199
  return null;
186
200
  }
187
- const valid = placeholders.every((name) => {
188
- const attribute = attributes[name];
189
- return Boolean(
190
- attribute && attribute.private !== true && SCALAR_TYPES.has(attribute.type ?? "")
191
- );
201
+ const valid = placeholders.every((path) => {
202
+ const segments = path.split(".");
203
+ let schema = targetSchema;
204
+ for (const [index2, segment] of segments.entries()) {
205
+ const attribute = schema.attributes?.[segment];
206
+ const isLastSegment = index2 === segments.length - 1;
207
+ if (index2 === 0 && isLastSegment && SYSTEM_SCALAR_FIELDS.has(segment)) {
208
+ return true;
209
+ }
210
+ if (!attribute || attribute.private === true) {
211
+ return false;
212
+ }
213
+ if (isLastSegment) {
214
+ return SCALAR_TYPES.has(attribute.type ?? "");
215
+ }
216
+ if (attribute.type !== "component" || !attribute.component || !resolveSchema) {
217
+ return false;
218
+ }
219
+ schema = resolveSchema(attribute.component);
220
+ if (!schema) {
221
+ return false;
222
+ }
223
+ }
224
+ return false;
192
225
  });
193
226
  if (!valid) {
194
227
  return null;
@@ -196,14 +229,14 @@ const compileTemplate = (template, targetSchema) => {
196
229
  return {
197
230
  template,
198
231
  placeholders,
199
- displayField: placeholders[0]
232
+ displayField: placeholders[0].split(".").slice(-1)[0] ?? placeholders[0]
200
233
  };
201
234
  };
202
235
  const renderTemplate = (compiled, values) => {
203
236
  const rendered = compiled.template.replace(
204
- /\{([A-Za-z_][A-Za-z0-9_]*)\}/g,
237
+ /\{([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)\}/g,
205
238
  (_match, name) => {
206
- const value = values[name];
239
+ const value = getPathValue(values, name);
207
240
  return value === null || value === void 0 ? "" : String(value);
208
241
  }
209
242
  );
@@ -270,28 +303,40 @@ const getRuntime = async (strapi, settings2, sourceUid, targetField, userAbility
270
303
  }
271
304
  const template = settings2.relations[sourceUid]?.[targetField];
272
305
  const targetSchema = strapi.getModel(attribute.target);
273
- const compiled = typeof template === "string" ? compileTemplate(template, targetSchema) : null;
306
+ const compiled = typeof template === "string" ? compileTemplate(
307
+ template,
308
+ targetSchema,
309
+ (uid) => strapi.getModel(uid)
310
+ ) : null;
274
311
  if (!compiled || !targetSchema) {
275
312
  return null;
276
313
  }
314
+ const originalMainField = await getOriginalMainField(
315
+ strapi,
316
+ sourceSchema,
317
+ targetSchema,
318
+ targetField,
319
+ userAbility
320
+ );
277
321
  return {
278
322
  ...compiled,
323
+ displayField: compiled.placeholders.some((placeholder) => placeholder.includes(".")) ? originalMainField : compiled.displayField,
279
324
  sourceUid,
280
325
  targetUid: targetSchema.uid,
281
- originalMainField: await getOriginalMainField(
282
- strapi,
283
- sourceSchema,
284
- targetSchema,
285
- targetField,
286
- userAbility
287
- )
326
+ originalMainField
288
327
  };
289
328
  };
329
+ const buildPopulate = (placeholders) => {
330
+ const roots = new Set(
331
+ placeholders.filter((placeholder) => placeholder.includes(".")).map((placeholder) => placeholder.split(".")[0])
332
+ );
333
+ return roots.size > 0 ? Object.fromEntries(Array.from(roots, (root) => [root, true])) : void 0;
334
+ };
290
335
  const hydrateValues = async (strapi, ctx, runtime, values) => {
291
336
  const targetSchema = strapi.getModel(runtime.targetUid);
292
337
  const targetModelType = targetSchema.modelType;
293
338
  const missingFields = runtime.placeholders.filter(
294
- (field) => values.some((value) => !Object.prototype.hasOwnProperty.call(value, field))
339
+ (field) => values.some((value) => getPathValue(value, field) === void 0)
295
340
  );
296
341
  if (missingFields.length === 0) {
297
342
  return new Map(values.map((value) => [value, value]));
@@ -304,7 +349,7 @@ const hydrateValues = async (strapi, ctx, runtime, values) => {
304
349
  const permissionChecker = strapi.plugin("content-manager").service("permission-checker").create({ userAbility: ctx.state.userAbility, model: runtime.targetUid });
305
350
  const fields = Array.from(
306
351
  /* @__PURE__ */ new Set([
307
- ...runtime.placeholders,
352
+ ...runtime.placeholders.filter((field) => !field.includes(".")),
308
353
  runtime.originalMainField,
309
354
  "id",
310
355
  "documentId",
@@ -312,10 +357,12 @@ const hydrateValues = async (strapi, ctx, runtime, values) => {
312
357
  "publishedAt"
313
358
  ])
314
359
  );
360
+ const populate = buildPopulate(runtime.placeholders);
315
361
  const identityField = targetModelType === "component" ? "id" : "documentId";
316
362
  const permissionQuery = await permissionChecker.sanitizedQuery.read({
317
363
  fields,
318
- filters: { [identityField]: { $in: identities } }
364
+ filters: { [identityField]: { $in: identities } },
365
+ ...populate ? { populate } : {}
319
366
  });
320
367
  const query = strapi.get("query-params").transform(runtime.targetUid, permissionQuery);
321
368
  const hydrated = await strapi.db.query(runtime.targetUid).findMany(query);
@@ -368,23 +415,27 @@ const relationLabels = ({ strapi }) => {
368
415
  const targetSchema = strapi.getModel(attribute.target);
369
416
  const compiled = compileTemplate(
370
417
  settings2.relations[sourceUid]?.[fieldName] ?? "",
371
- targetSchema
418
+ targetSchema,
419
+ (uid) => strapi.getModel(uid)
372
420
  );
373
421
  if (!compiled || !isRecord$1(metadata)) {
374
422
  continue;
375
423
  }
424
+ const editMetadata = isRecord$1(metadata.edit) ? metadata.edit : {};
425
+ const configuredMainField = typeof editMetadata.mainField === "string" ? editMetadata.mainField : "id";
426
+ const displayField = compiled.placeholders.some((placeholder) => placeholder.includes(".")) ? configuredMainField : compiled.displayField;
376
427
  metadatas[fieldName] = {
377
428
  ...metadata,
378
429
  edit: {
379
- ...isRecord$1(metadata.edit) ? metadata.edit : {},
380
- mainField: compiled.displayField
430
+ ...editMetadata,
431
+ mainField: displayField
381
432
  },
382
433
  list: {
383
434
  ...isRecord$1(metadata.list) ? metadata.list : {},
384
- mainField: compiled.displayField
435
+ mainField: displayField
385
436
  }
386
437
  };
387
- relationNames[fieldName] = { mainField: compiled.displayField };
438
+ relationNames[fieldName] = { mainField: displayField };
388
439
  }
389
440
  return {
390
441
  ...configuration,
@@ -48,7 +48,7 @@ declare const _default: {
48
48
  decorateCollectionResults: (ctx: any, sourceUid: string, results: unknown[]) => Promise<unknown[]>;
49
49
  decorateConfiguration: (data: any, sourceUid: string) => Promise<any>;
50
50
  decorateRelationResults: (ctx: any, sourceUid: string, targetField: string, results: unknown[]) => Promise<unknown[]>;
51
- compileTemplate: (template: string, targetSchema: import('./services/utils/relation-labels').SchemaLike | undefined) => import('./services/utils/relation-labels').CompiledTemplate | null;
51
+ compileTemplate: (template: string, targetSchema: import('./services/utils/relation-labels').SchemaLike | undefined, resolveSchema?: import('./services/utils/relation-labels').SchemaResolver) => import('./services/utils/relation-labels').CompiledTemplate | null;
52
52
  parseTemplate: (template: string) => string[] | null;
53
53
  renderTemplate: (compiled: import('./services/utils/relation-labels').CompiledTemplate, values: Record<string, unknown>) => string;
54
54
  };
@@ -5,7 +5,7 @@ declare const _default: {
5
5
  decorateCollectionResults: (ctx: any, sourceUid: string, results: unknown[]) => Promise<unknown[]>;
6
6
  decorateConfiguration: (data: any, sourceUid: string) => Promise<any>;
7
7
  decorateRelationResults: (ctx: any, sourceUid: string, targetField: string, results: unknown[]) => Promise<unknown[]>;
8
- compileTemplate: (template: string, targetSchema: import('./utils/relation-labels').SchemaLike | undefined) => import('./utils/relation-labels').CompiledTemplate | null;
8
+ compileTemplate: (template: string, targetSchema: import('./utils/relation-labels').SchemaLike | undefined, resolveSchema?: import('./utils/relation-labels').SchemaResolver) => import('./utils/relation-labels').CompiledTemplate | null;
9
9
  parseTemplate: (template: string) => string[] | null;
10
10
  renderTemplate: (compiled: import('./utils/relation-labels').CompiledTemplate, values: Record<string, unknown>) => string;
11
11
  };
@@ -1,14 +1,16 @@
1
1
  import { Core } from '@strapi/strapi';
2
2
  import { SchemaLike } from './utils/relation-labels';
3
+ declare const buildPopulate: (placeholders: string[]) => Record<string, true> | undefined;
3
4
  declare const relationLabels: ({ strapi }: {
4
5
  strapi: Core.Strapi;
5
6
  }) => {
6
7
  decorateCollectionResults: (ctx: any, sourceUid: string, results: unknown[]) => Promise<unknown[]>;
7
8
  decorateConfiguration: (data: any, sourceUid: string) => Promise<any>;
8
9
  decorateRelationResults: (ctx: any, sourceUid: string, targetField: string, results: unknown[]) => Promise<unknown[]>;
9
- compileTemplate: (template: string, targetSchema: SchemaLike | undefined) => import('./utils/relation-labels').CompiledTemplate | null;
10
+ compileTemplate: (template: string, targetSchema: SchemaLike | undefined, resolveSchema?: import('./utils/relation-labels').SchemaResolver) => import('./utils/relation-labels').CompiledTemplate | null;
10
11
  parseTemplate: (template: string) => string[] | null;
11
12
  renderTemplate: (compiled: import('./utils/relation-labels').CompiledTemplate, values: Record<string, unknown>) => string;
12
13
  };
14
+ export { buildPopulate };
13
15
  export { compileTemplate, parseTemplate, renderTemplate } from './utils/relation-labels';
14
16
  export default relationLabels;
@@ -11,6 +11,7 @@ export type SchemaLike = {
11
11
  modelType?: string;
12
12
  attributes?: Record<string, SchemaAttribute>;
13
13
  };
14
+ export type SchemaResolver = (uid: string) => SchemaLike | undefined;
14
15
  export type CompiledTemplate = {
15
16
  template: string;
16
17
  placeholders: string[];
@@ -22,6 +23,7 @@ export type RelationRuntime = CompiledTemplate & {
22
23
  originalMainField: string;
23
24
  };
24
25
  export declare const isRecord: (value: unknown) => value is Record<string, unknown>;
26
+ export declare const getPathValue: (value: unknown, path: string) => unknown;
25
27
  export declare const parseTemplate: (template: string) => string[] | null;
26
- export declare const compileTemplate: (template: string, targetSchema: SchemaLike | undefined) => CompiledTemplate | null;
28
+ export declare const compileTemplate: (template: string, targetSchema: SchemaLike | undefined, resolveSchema?: SchemaResolver) => CompiledTemplate | null;
27
29
  export declare const renderTemplate: (compiled: CompiledTemplate, values: Record<string, unknown>) => string;
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "author": "Christoph Tupi <christoph.tupi@gmail.com>",
6
6
  "keywords": [],
7
7
  "type": "commonjs",
8
- "version": "0.0.0",
8
+ "version": "1.1.0",
9
9
  "strapi": {
10
10
  "kind": "plugin",
11
11
  "name": "strapi-relation-names",