strapi-relation-names 1.0.0 → 1.2.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
@@ -138,13 +142,7 @@ Customer: {firstName} {lastName}
138
142
 
139
143
  ## 🗂️ How It Works
140
144
 
141
- The plugin changes how relations are displayed inside the Strapi Admin panel.
142
-
143
- The first placeholder in a template is used internally by Strapi's Admin relation renderer.
144
-
145
- Strapi's existing relation search and sorting continue to use the configured default main field.
146
-
147
- 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.
148
146
 
149
147
  For example:
150
148
 
@@ -172,9 +170,11 @@ The default label is also used when:
172
170
 
173
171
  - [ ] Live label previews while editing templates
174
172
  - [ ] Define default relation templates in plugin configuration
175
- - [ ] Support nested field paths in templates
173
+ - [x] Support nested field paths in templates
174
+ - [x] Support nested relation fields in templates
176
175
  - [ ] Import and export relation-label settings
177
176
  - [ ] Conditional labels and configurable fallback rules
177
+ - [ ] Support label transformation functions (e.g., `toUpperCase`, `toLowerCase`, `capitalize`, etc.)
178
178
 
179
179
  If you have any feature requests or suggestions, please open a dedicated issue.
180
180
 
@@ -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-DFr0KRky.js");
9
9
  const FIELD_TYPES = /* @__PURE__ */ new Set([
10
10
  "string",
11
11
  "text",
@@ -21,6 +21,25 @@ 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_]*)*$/;
34
+ const getNestedSchemaUid = (attribute) => {
35
+ if (attribute.type === "component") {
36
+ return attribute.component;
37
+ }
38
+ if (attribute.type === "relation") {
39
+ return attribute.targetModel ?? attribute.target;
40
+ }
41
+ return void 0;
42
+ };
24
43
  const getPlaceholders = (template) => {
25
44
  if (!template.trim()) {
26
45
  return null;
@@ -33,7 +52,7 @@ const getPlaceholders = (template) => {
33
52
  return null;
34
53
  }
35
54
  const name = match[1];
36
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
55
+ if (!PLACEHOLDER_PATTERN.test(name)) {
37
56
  return null;
38
57
  }
39
58
  placeholders.push(name);
@@ -44,16 +63,62 @@ const getPlaceholders = (template) => {
44
63
  }
45
64
  return placeholders.length > 0 ? placeholders : null;
46
65
  };
47
- const validateTemplate = (template, attributes) => {
66
+ const validateTemplate = (template, attributes, schemas) => {
48
67
  const placeholders = getPlaceholders(template);
49
68
  if (!placeholders) {
50
69
  return "Use at least one valid {fieldName} placeholder.";
51
70
  }
52
- const invalidField = placeholders.find(
53
- (field) => !FIELD_TYPES.has(attributes?.[field]?.type ?? "")
54
- );
71
+ const invalidField = placeholders.find((path) => {
72
+ const segments = path.split(".");
73
+ let currentAttributes = attributes;
74
+ for (const [index2, segment] of segments.entries()) {
75
+ if (index2 === 0 && segments.length === 1 && SYSTEM_SCALAR_FIELDS.includes(segment)) {
76
+ return false;
77
+ }
78
+ const attribute = currentAttributes?.[segment];
79
+ const isLastSegment = index2 === segments.length - 1;
80
+ if (!attribute || attribute.private === true) {
81
+ return true;
82
+ }
83
+ if (isLastSegment) {
84
+ return !FIELD_TYPES.has(attribute.type ?? "");
85
+ }
86
+ const nestedSchemaUid = getNestedSchemaUid(attribute);
87
+ if (!nestedSchemaUid) {
88
+ return true;
89
+ }
90
+ currentAttributes = schemas?.get(nestedSchemaUid)?.attributes;
91
+ }
92
+ return true;
93
+ });
55
94
  return invalidField ? `“${invalidField}” is not an available scalar field.` : null;
56
95
  };
96
+ const getAvailableFields = (schema, schemas, prefix = "", ancestors = /* @__PURE__ */ new Set()) => {
97
+ if (!schema) {
98
+ return [];
99
+ }
100
+ const fields = Object.entries(schema.attributes ?? {}).flatMap(([name, attribute]) => {
101
+ if (attribute.private === true) {
102
+ return [];
103
+ }
104
+ const path = `${prefix}${name}`;
105
+ if (FIELD_TYPES.has(attribute.type ?? "")) {
106
+ return [path];
107
+ }
108
+ const nestedSchemaUid = getNestedSchemaUid(attribute);
109
+ if (!nestedSchemaUid || ancestors.has(nestedSchemaUid)) {
110
+ return [];
111
+ }
112
+ return getAvailableFields(
113
+ schemas.get(nestedSchemaUid),
114
+ schemas,
115
+ `${path}.`,
116
+ /* @__PURE__ */ new Set([...ancestors, nestedSchemaUid])
117
+ );
118
+ });
119
+ const systemFields = prefix === "" ? SYSTEM_SCALAR_FIELDS : [];
120
+ return [...systemFields, ...fields.filter((field) => !systemFields.includes(field))];
121
+ };
57
122
  const PLUGIN_SETTINGS_PATH = "/strapi-relation-names/settings";
58
123
  const getRelationTarget = (attribute) => attribute.targetModel ?? attribute.target;
59
124
  const SettingsPage = () => {
@@ -143,7 +208,7 @@ const SettingsPage = () => {
143
208
  !isLoading ? rows.map(({ source, fieldName, target }) => {
144
209
  const targetAttributes = target?.attributes ?? {};
145
210
  const template = settings.relations[source.uid]?.[fieldName] ?? "";
146
- const validationError = template ? validateTemplate(template, targetAttributes) : null;
211
+ const validationError = template ? validateTemplate(template, targetAttributes, schemaMap) : null;
147
212
  return /* @__PURE__ */ jsxRuntime.jsxs(
148
213
  designSystem.Box,
149
214
  {
@@ -186,7 +251,7 @@ const SettingsPage = () => {
186
251
  formatMessage({ id: index.getTranslation("settings.availableFields") }),
187
252
  ":",
188
253
  " ",
189
- Object.keys(targetAttributes).join(", ")
254
+ getAvailableFields(target, schemaMap).join(", ")
190
255
  ] }) })
191
256
  ]
192
257
  },
@@ -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-DdvdGn6T.mjs";
7
7
  const FIELD_TYPES = /* @__PURE__ */ new Set([
8
8
  "string",
9
9
  "text",
@@ -19,6 +19,25 @@ 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_]*)*$/;
32
+ const getNestedSchemaUid = (attribute) => {
33
+ if (attribute.type === "component") {
34
+ return attribute.component;
35
+ }
36
+ if (attribute.type === "relation") {
37
+ return attribute.targetModel ?? attribute.target;
38
+ }
39
+ return void 0;
40
+ };
22
41
  const getPlaceholders = (template) => {
23
42
  if (!template.trim()) {
24
43
  return null;
@@ -31,7 +50,7 @@ const getPlaceholders = (template) => {
31
50
  return null;
32
51
  }
33
52
  const name = match[1];
34
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
53
+ if (!PLACEHOLDER_PATTERN.test(name)) {
35
54
  return null;
36
55
  }
37
56
  placeholders.push(name);
@@ -42,16 +61,62 @@ const getPlaceholders = (template) => {
42
61
  }
43
62
  return placeholders.length > 0 ? placeholders : null;
44
63
  };
45
- const validateTemplate = (template, attributes) => {
64
+ const validateTemplate = (template, attributes, schemas) => {
46
65
  const placeholders = getPlaceholders(template);
47
66
  if (!placeholders) {
48
67
  return "Use at least one valid {fieldName} placeholder.";
49
68
  }
50
- const invalidField = placeholders.find(
51
- (field) => !FIELD_TYPES.has(attributes?.[field]?.type ?? "")
52
- );
69
+ const invalidField = placeholders.find((path) => {
70
+ const segments = path.split(".");
71
+ let currentAttributes = attributes;
72
+ for (const [index, segment] of segments.entries()) {
73
+ if (index === 0 && segments.length === 1 && SYSTEM_SCALAR_FIELDS.includes(segment)) {
74
+ return false;
75
+ }
76
+ const attribute = currentAttributes?.[segment];
77
+ const isLastSegment = index === segments.length - 1;
78
+ if (!attribute || attribute.private === true) {
79
+ return true;
80
+ }
81
+ if (isLastSegment) {
82
+ return !FIELD_TYPES.has(attribute.type ?? "");
83
+ }
84
+ const nestedSchemaUid = getNestedSchemaUid(attribute);
85
+ if (!nestedSchemaUid) {
86
+ return true;
87
+ }
88
+ currentAttributes = schemas?.get(nestedSchemaUid)?.attributes;
89
+ }
90
+ return true;
91
+ });
53
92
  return invalidField ? `“${invalidField}” is not an available scalar field.` : null;
54
93
  };
94
+ const getAvailableFields = (schema, schemas, prefix = "", ancestors = /* @__PURE__ */ new Set()) => {
95
+ if (!schema) {
96
+ return [];
97
+ }
98
+ const fields = Object.entries(schema.attributes ?? {}).flatMap(([name, attribute]) => {
99
+ if (attribute.private === true) {
100
+ return [];
101
+ }
102
+ const path = `${prefix}${name}`;
103
+ if (FIELD_TYPES.has(attribute.type ?? "")) {
104
+ return [path];
105
+ }
106
+ const nestedSchemaUid = getNestedSchemaUid(attribute);
107
+ if (!nestedSchemaUid || ancestors.has(nestedSchemaUid)) {
108
+ return [];
109
+ }
110
+ return getAvailableFields(
111
+ schemas.get(nestedSchemaUid),
112
+ schemas,
113
+ `${path}.`,
114
+ /* @__PURE__ */ new Set([...ancestors, nestedSchemaUid])
115
+ );
116
+ });
117
+ const systemFields = prefix === "" ? SYSTEM_SCALAR_FIELDS : [];
118
+ return [...systemFields, ...fields.filter((field) => !systemFields.includes(field))];
119
+ };
55
120
  const PLUGIN_SETTINGS_PATH = "/strapi-relation-names/settings";
56
121
  const getRelationTarget = (attribute) => attribute.targetModel ?? attribute.target;
57
122
  const SettingsPage = () => {
@@ -141,7 +206,7 @@ const SettingsPage = () => {
141
206
  !isLoading ? rows.map(({ source, fieldName, target }) => {
142
207
  const targetAttributes = target?.attributes ?? {};
143
208
  const template = settings.relations[source.uid]?.[fieldName] ?? "";
144
- const validationError = template ? validateTemplate(template, targetAttributes) : null;
209
+ const validationError = template ? validateTemplate(template, targetAttributes, schemaMap) : null;
145
210
  return /* @__PURE__ */ jsxs(
146
211
  Box,
147
212
  {
@@ -184,7 +249,7 @@ const SettingsPage = () => {
184
249
  formatMessage({ id: getTranslation("settings.availableFields") }),
185
250
  ":",
186
251
  " ",
187
- Object.keys(targetAttributes).join(", ")
252
+ getAvailableFields(target, schemaMap).join(", ")
188
253
  ] }) })
189
254
  ]
190
255
  },
@@ -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-BGsuRnwx.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-Blk-4H_X.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-DFr0KRky.js");
4
4
  exports.default = index.plugin;
@@ -1,4 +1,4 @@
1
- import { p } from "./index-CJO9wtUk.mjs";
1
+ import { p } from "./index-DdvdGn6T.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,14 @@
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
+ target?: string;
5
+ targetModel?: string;
6
+ private?: boolean;
7
+ };
8
+ type TemplateSchema = {
9
+ attributes?: Record<string, TemplateAttribute>;
10
+ };
11
+ declare const getPlaceholders: (template: string) => string[] | null;
12
+ declare const validateTemplate: (template: string, attributes: Record<string, TemplateAttribute> | undefined, schemas?: ReadonlyMap<string, TemplateSchema>) => string | null;
13
+ declare const getAvailableFields: (schema: TemplateSchema | undefined, schemas: ReadonlyMap<string, TemplateSchema>, prefix?: string, ancestors?: Set<string>) => string[];
14
+ export { getAvailableFields, getPlaceholders, validateTemplate };
@@ -151,7 +151,25 @@ 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
+ if (Array.isArray(current)) {
168
+ return current.map((item) => isRecord$1(item) ? item[segment] : void 0).filter((item) => item !== void 0);
169
+ }
170
+ return isRecord$1(current) ? current[segment] : void 0;
171
+ }, value);
172
+ };
155
173
  const parseTemplate = (template) => {
156
174
  if (!template.trim()) {
157
175
  return null;
@@ -172,7 +190,7 @@ const parseTemplate = (template) => {
172
190
  return null;
173
191
  }
174
192
  const name = template.slice(openingBrace + 1, end);
175
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
193
+ if (!PLACEHOLDER_PATTERN.test(name)) {
176
194
  return null;
177
195
  }
178
196
  placeholders.push(name);
@@ -180,17 +198,36 @@ const parseTemplate = (template) => {
180
198
  }
181
199
  return placeholders.length > 0 ? placeholders : null;
182
200
  };
183
- const compileTemplate = (template, targetSchema) => {
201
+ const compileTemplate = (template, targetSchema, resolveSchema) => {
184
202
  const placeholders = parseTemplate(template);
185
- const attributes = targetSchema?.attributes ?? {};
186
203
  if (!placeholders || !targetSchema) {
187
204
  return null;
188
205
  }
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
- );
206
+ const valid = placeholders.every((path) => {
207
+ const segments = path.split(".");
208
+ let schema = targetSchema;
209
+ for (const [index2, segment] of segments.entries()) {
210
+ const attribute = schema.attributes?.[segment];
211
+ const isLastSegment = index2 === segments.length - 1;
212
+ if (index2 === 0 && isLastSegment && SYSTEM_SCALAR_FIELDS.has(segment)) {
213
+ return true;
214
+ }
215
+ if (!attribute || attribute.private === true) {
216
+ return false;
217
+ }
218
+ if (isLastSegment) {
219
+ return SCALAR_TYPES.has(attribute.type ?? "");
220
+ }
221
+ const nestedSchemaUid = attribute.type === "component" ? attribute.component : attribute.type === "relation" ? attribute.target : void 0;
222
+ if (!nestedSchemaUid || !resolveSchema) {
223
+ return false;
224
+ }
225
+ schema = resolveSchema(nestedSchemaUid);
226
+ if (!schema) {
227
+ return false;
228
+ }
229
+ }
230
+ return false;
194
231
  });
195
232
  if (!valid) {
196
233
  return null;
@@ -198,14 +235,14 @@ const compileTemplate = (template, targetSchema) => {
198
235
  return {
199
236
  template,
200
237
  placeholders,
201
- displayField: placeholders[0]
238
+ displayField: placeholders[0].split(".").slice(-1)[0] ?? placeholders[0]
202
239
  };
203
240
  };
204
241
  const renderTemplate = (compiled, values) => {
205
242
  const rendered = compiled.template.replace(
206
- /\{([A-Za-z_][A-Za-z0-9_]*)\}/g,
243
+ /\{([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)\}/g,
207
244
  (_match, name) => {
208
- const value = values[name];
245
+ const value = getPathValue(values, name);
209
246
  return value === null || value === void 0 ? "" : String(value);
210
247
  }
211
248
  );
@@ -272,28 +309,62 @@ const getRuntime = async (strapi, settings2, sourceUid, targetField, userAbility
272
309
  }
273
310
  const template = settings2.relations[sourceUid]?.[targetField];
274
311
  const targetSchema = strapi.getModel(attribute.target);
275
- const compiled = typeof template === "string" ? compileTemplate(template, targetSchema) : null;
312
+ const compiled = typeof template === "string" ? compileTemplate(
313
+ template,
314
+ targetSchema,
315
+ (uid) => strapi.getModel(uid)
316
+ ) : null;
276
317
  if (!compiled || !targetSchema) {
277
318
  return null;
278
319
  }
320
+ const originalMainField = await getOriginalMainField(
321
+ strapi,
322
+ sourceSchema,
323
+ targetSchema,
324
+ targetField,
325
+ userAbility
326
+ );
279
327
  return {
280
328
  ...compiled,
329
+ displayField: compiled.placeholders.some((placeholder) => placeholder.includes(".")) ? originalMainField : compiled.displayField,
281
330
  sourceUid,
282
331
  targetUid: targetSchema.uid,
283
- originalMainField: await getOriginalMainField(
284
- strapi,
285
- sourceSchema,
286
- targetSchema,
287
- targetField,
288
- userAbility
289
- )
332
+ originalMainField
290
333
  };
291
334
  };
335
+ const buildPopulate = (placeholders) => {
336
+ const populate = {};
337
+ for (const placeholder of placeholders) {
338
+ const segments = placeholder.split(".");
339
+ if (segments.length < 2) {
340
+ continue;
341
+ }
342
+ let current = populate;
343
+ for (const [index2, segment] of segments.slice(0, -1).entries()) {
344
+ const existing = current[segment];
345
+ if (existing === true) {
346
+ break;
347
+ }
348
+ if (index2 === segments.length - 2) {
349
+ current[segment] = existing ?? true;
350
+ break;
351
+ }
352
+ if (!existing) {
353
+ current[segment] = { populate: {} };
354
+ }
355
+ const next = current[segment];
356
+ if (next !== true) {
357
+ current = next.populate;
358
+ }
359
+ }
360
+ }
361
+ return Object.keys(populate).length > 0 ? populate : void 0;
362
+ };
292
363
  const hydrateValues = async (strapi, ctx, runtime, values) => {
293
364
  const targetSchema = strapi.getModel(runtime.targetUid);
294
365
  const targetModelType = targetSchema.modelType;
295
366
  const missingFields = runtime.placeholders.filter(
296
- (field) => values.some((value) => !Object.prototype.hasOwnProperty.call(value, field))
367
+ (field) => values.some((value) => getPathValue(value, field) === void 0)
297
368
  );
298
369
  if (missingFields.length === 0) {
299
370
  return new Map(values.map((value) => [value, value]));
@@ -306,7 +377,7 @@ const hydrateValues = async (strapi, ctx, runtime, values) => {
306
377
  const permissionChecker = strapi.plugin("content-manager").service("permission-checker").create({ userAbility: ctx.state.userAbility, model: runtime.targetUid });
307
378
  const fields = Array.from(
308
379
  /* @__PURE__ */ new Set([
309
- ...runtime.placeholders,
380
+ ...runtime.placeholders.filter((field) => !field.includes(".")),
310
381
  runtime.originalMainField,
311
382
  "id",
312
383
  "documentId",
@@ -314,10 +385,12 @@ const hydrateValues = async (strapi, ctx, runtime, values) => {
314
385
  "publishedAt"
315
386
  ])
316
387
  );
388
+ const populate = buildPopulate(runtime.placeholders);
317
389
  const identityField = targetModelType === "component" ? "id" : "documentId";
318
390
  const permissionQuery = await permissionChecker.sanitizedQuery.read({
319
391
  fields,
320
- filters: { [identityField]: { $in: identities } }
392
+ filters: { [identityField]: { $in: identities } },
393
+ ...populate ? { populate } : {}
321
394
  });
322
395
  const query = strapi.get("query-params").transform(runtime.targetUid, permissionQuery);
323
396
  const hydrated = await strapi.db.query(runtime.targetUid).findMany(query);
@@ -370,23 +443,27 @@ const relationLabels = ({ strapi }) => {
370
443
  const targetSchema = strapi.getModel(attribute.target);
371
444
  const compiled = compileTemplate(
372
445
  settings2.relations[sourceUid]?.[fieldName] ?? "",
373
- targetSchema
446
+ targetSchema,
447
+ (uid) => strapi.getModel(uid)
374
448
  );
375
449
  if (!compiled || !isRecord$1(metadata)) {
376
450
  continue;
377
451
  }
452
+ const editMetadata = isRecord$1(metadata.edit) ? metadata.edit : {};
453
+ const configuredMainField = typeof editMetadata.mainField === "string" ? editMetadata.mainField : "id";
454
+ const displayField = compiled.placeholders.some((placeholder) => placeholder.includes(".")) ? configuredMainField : compiled.displayField;
378
455
  metadatas[fieldName] = {
379
456
  ...metadata,
380
457
  edit: {
381
- ...isRecord$1(metadata.edit) ? metadata.edit : {},
382
- mainField: compiled.displayField
458
+ ...editMetadata,
459
+ mainField: displayField
383
460
  },
384
461
  list: {
385
462
  ...isRecord$1(metadata.list) ? metadata.list : {},
386
- mainField: compiled.displayField
463
+ mainField: displayField
387
464
  }
388
465
  };
389
- relationNames[fieldName] = { mainField: compiled.displayField };
466
+ relationNames[fieldName] = { mainField: displayField };
390
467
  }
391
468
  return {
392
469
  ...configuration,
@@ -149,7 +149,25 @@ 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
+ if (Array.isArray(current)) {
166
+ return current.map((item) => isRecord$1(item) ? item[segment] : void 0).filter((item) => item !== void 0);
167
+ }
168
+ return isRecord$1(current) ? current[segment] : void 0;
169
+ }, value);
170
+ };
153
171
  const parseTemplate = (template) => {
154
172
  if (!template.trim()) {
155
173
  return null;
@@ -170,7 +188,7 @@ const parseTemplate = (template) => {
170
188
  return null;
171
189
  }
172
190
  const name = template.slice(openingBrace + 1, end);
173
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
191
+ if (!PLACEHOLDER_PATTERN.test(name)) {
174
192
  return null;
175
193
  }
176
194
  placeholders.push(name);
@@ -178,17 +196,36 @@ const parseTemplate = (template) => {
178
196
  }
179
197
  return placeholders.length > 0 ? placeholders : null;
180
198
  };
181
- const compileTemplate = (template, targetSchema) => {
199
+ const compileTemplate = (template, targetSchema, resolveSchema) => {
182
200
  const placeholders = parseTemplate(template);
183
- const attributes = targetSchema?.attributes ?? {};
184
201
  if (!placeholders || !targetSchema) {
185
202
  return null;
186
203
  }
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
- );
204
+ const valid = placeholders.every((path) => {
205
+ const segments = path.split(".");
206
+ let schema = targetSchema;
207
+ for (const [index2, segment] of segments.entries()) {
208
+ const attribute = schema.attributes?.[segment];
209
+ const isLastSegment = index2 === segments.length - 1;
210
+ if (index2 === 0 && isLastSegment && SYSTEM_SCALAR_FIELDS.has(segment)) {
211
+ return true;
212
+ }
213
+ if (!attribute || attribute.private === true) {
214
+ return false;
215
+ }
216
+ if (isLastSegment) {
217
+ return SCALAR_TYPES.has(attribute.type ?? "");
218
+ }
219
+ const nestedSchemaUid = attribute.type === "component" ? attribute.component : attribute.type === "relation" ? attribute.target : void 0;
220
+ if (!nestedSchemaUid || !resolveSchema) {
221
+ return false;
222
+ }
223
+ schema = resolveSchema(nestedSchemaUid);
224
+ if (!schema) {
225
+ return false;
226
+ }
227
+ }
228
+ return false;
192
229
  });
193
230
  if (!valid) {
194
231
  return null;
@@ -196,14 +233,14 @@ const compileTemplate = (template, targetSchema) => {
196
233
  return {
197
234
  template,
198
235
  placeholders,
199
- displayField: placeholders[0]
236
+ displayField: placeholders[0].split(".").slice(-1)[0] ?? placeholders[0]
200
237
  };
201
238
  };
202
239
  const renderTemplate = (compiled, values) => {
203
240
  const rendered = compiled.template.replace(
204
- /\{([A-Za-z_][A-Za-z0-9_]*)\}/g,
241
+ /\{([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)\}/g,
205
242
  (_match, name) => {
206
- const value = values[name];
243
+ const value = getPathValue(values, name);
207
244
  return value === null || value === void 0 ? "" : String(value);
208
245
  }
209
246
  );
@@ -270,28 +307,62 @@ const getRuntime = async (strapi, settings2, sourceUid, targetField, userAbility
270
307
  }
271
308
  const template = settings2.relations[sourceUid]?.[targetField];
272
309
  const targetSchema = strapi.getModel(attribute.target);
273
- const compiled = typeof template === "string" ? compileTemplate(template, targetSchema) : null;
310
+ const compiled = typeof template === "string" ? compileTemplate(
311
+ template,
312
+ targetSchema,
313
+ (uid) => strapi.getModel(uid)
314
+ ) : null;
274
315
  if (!compiled || !targetSchema) {
275
316
  return null;
276
317
  }
318
+ const originalMainField = await getOriginalMainField(
319
+ strapi,
320
+ sourceSchema,
321
+ targetSchema,
322
+ targetField,
323
+ userAbility
324
+ );
277
325
  return {
278
326
  ...compiled,
327
+ displayField: compiled.placeholders.some((placeholder) => placeholder.includes(".")) ? originalMainField : compiled.displayField,
279
328
  sourceUid,
280
329
  targetUid: targetSchema.uid,
281
- originalMainField: await getOriginalMainField(
282
- strapi,
283
- sourceSchema,
284
- targetSchema,
285
- targetField,
286
- userAbility
287
- )
330
+ originalMainField
288
331
  };
289
332
  };
333
+ const buildPopulate = (placeholders) => {
334
+ const populate = {};
335
+ for (const placeholder of placeholders) {
336
+ const segments = placeholder.split(".");
337
+ if (segments.length < 2) {
338
+ continue;
339
+ }
340
+ let current = populate;
341
+ for (const [index2, segment] of segments.slice(0, -1).entries()) {
342
+ const existing = current[segment];
343
+ if (existing === true) {
344
+ break;
345
+ }
346
+ if (index2 === segments.length - 2) {
347
+ current[segment] = existing ?? true;
348
+ break;
349
+ }
350
+ if (!existing) {
351
+ current[segment] = { populate: {} };
352
+ }
353
+ const next = current[segment];
354
+ if (next !== true) {
355
+ current = next.populate;
356
+ }
357
+ }
358
+ }
359
+ return Object.keys(populate).length > 0 ? populate : void 0;
360
+ };
290
361
  const hydrateValues = async (strapi, ctx, runtime, values) => {
291
362
  const targetSchema = strapi.getModel(runtime.targetUid);
292
363
  const targetModelType = targetSchema.modelType;
293
364
  const missingFields = runtime.placeholders.filter(
294
- (field) => values.some((value) => !Object.prototype.hasOwnProperty.call(value, field))
365
+ (field) => values.some((value) => getPathValue(value, field) === void 0)
295
366
  );
296
367
  if (missingFields.length === 0) {
297
368
  return new Map(values.map((value) => [value, value]));
@@ -304,7 +375,7 @@ const hydrateValues = async (strapi, ctx, runtime, values) => {
304
375
  const permissionChecker = strapi.plugin("content-manager").service("permission-checker").create({ userAbility: ctx.state.userAbility, model: runtime.targetUid });
305
376
  const fields = Array.from(
306
377
  /* @__PURE__ */ new Set([
307
- ...runtime.placeholders,
378
+ ...runtime.placeholders.filter((field) => !field.includes(".")),
308
379
  runtime.originalMainField,
309
380
  "id",
310
381
  "documentId",
@@ -312,10 +383,12 @@ const hydrateValues = async (strapi, ctx, runtime, values) => {
312
383
  "publishedAt"
313
384
  ])
314
385
  );
386
+ const populate = buildPopulate(runtime.placeholders);
315
387
  const identityField = targetModelType === "component" ? "id" : "documentId";
316
388
  const permissionQuery = await permissionChecker.sanitizedQuery.read({
317
389
  fields,
318
- filters: { [identityField]: { $in: identities } }
390
+ filters: { [identityField]: { $in: identities } },
391
+ ...populate ? { populate } : {}
319
392
  });
320
393
  const query = strapi.get("query-params").transform(runtime.targetUid, permissionQuery);
321
394
  const hydrated = await strapi.db.query(runtime.targetUid).findMany(query);
@@ -368,23 +441,27 @@ const relationLabels = ({ strapi }) => {
368
441
  const targetSchema = strapi.getModel(attribute.target);
369
442
  const compiled = compileTemplate(
370
443
  settings2.relations[sourceUid]?.[fieldName] ?? "",
371
- targetSchema
444
+ targetSchema,
445
+ (uid) => strapi.getModel(uid)
372
446
  );
373
447
  if (!compiled || !isRecord$1(metadata)) {
374
448
  continue;
375
449
  }
450
+ const editMetadata = isRecord$1(metadata.edit) ? metadata.edit : {};
451
+ const configuredMainField = typeof editMetadata.mainField === "string" ? editMetadata.mainField : "id";
452
+ const displayField = compiled.placeholders.some((placeholder) => placeholder.includes(".")) ? configuredMainField : compiled.displayField;
376
453
  metadatas[fieldName] = {
377
454
  ...metadata,
378
455
  edit: {
379
- ...isRecord$1(metadata.edit) ? metadata.edit : {},
380
- mainField: compiled.displayField
456
+ ...editMetadata,
457
+ mainField: displayField
381
458
  },
382
459
  list: {
383
460
  ...isRecord$1(metadata.list) ? metadata.list : {},
384
- mainField: compiled.displayField
461
+ mainField: displayField
385
462
  }
386
463
  };
387
- relationNames[fieldName] = { mainField: compiled.displayField };
464
+ relationNames[fieldName] = { mainField: displayField };
388
465
  }
389
466
  return {
390
467
  ...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,19 @@
1
1
  import { Core } from '@strapi/strapi';
2
2
  import { SchemaLike } from './utils/relation-labels';
3
+ type Populate = Record<string, true | {
4
+ populate: Populate;
5
+ }>;
6
+ declare const buildPopulate: (placeholders: string[]) => Populate | undefined;
3
7
  declare const relationLabels: ({ strapi }: {
4
8
  strapi: Core.Strapi;
5
9
  }) => {
6
10
  decorateCollectionResults: (ctx: any, sourceUid: string, results: unknown[]) => Promise<unknown[]>;
7
11
  decorateConfiguration: (data: any, sourceUid: string) => Promise<any>;
8
12
  decorateRelationResults: (ctx: any, sourceUid: string, targetField: string, results: unknown[]) => Promise<unknown[]>;
9
- compileTemplate: (template: string, targetSchema: SchemaLike | undefined) => import('./utils/relation-labels').CompiledTemplate | null;
13
+ compileTemplate: (template: string, targetSchema: SchemaLike | undefined, resolveSchema?: import('./utils/relation-labels').SchemaResolver) => import('./utils/relation-labels').CompiledTemplate | null;
10
14
  parseTemplate: (template: string) => string[] | null;
11
15
  renderTemplate: (compiled: import('./utils/relation-labels').CompiledTemplate, values: Record<string, unknown>) => string;
12
16
  };
17
+ export { buildPopulate };
13
18
  export { compileTemplate, parseTemplate, renderTemplate } from './utils/relation-labels';
14
19
  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": "1.0.0",
8
+ "version": "1.2.0",
9
9
  "strapi": {
10
10
  "kind": "plugin",
11
11
  "name": "strapi-relation-names",