@quazardous/qdadm 2.1.0 → 2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quazardous/qdadm",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Vue 3 framework for admin dashboards with PrimeVue",
5
5
  "author": "quazardous",
6
6
  "license": "MIT",
@@ -192,6 +192,26 @@ export interface KernelOptions {
192
192
  * wiring. Typically the same client your ApiStorages use.
193
193
  */
194
194
  apiClient?: ApiClientSource
195
+ /**
196
+ * Naming policy for the URL param carrying the parent id in child route
197
+ * families (#1201). `auto` (default): list-only families keep the bare
198
+ * `idField` param; families with show/edit routes namespace the parent
199
+ * (`jobs/:jobId/tasks/:id`) so the principal (child) id stays `:id`.
200
+ * `always`: namespaced everywhere. `bare`: legacy `idField` param, with
201
+ * an explicit registration error on collision.
202
+ */
203
+ parentParamMode?: 'auto' | 'always' | 'bare'
204
+ /**
205
+ * Global mapping service for parent URL param names (#1201). Consulted
206
+ * before `parentParamMode`; a per-call `parentParam` still wins. Return
207
+ * undefined to fall through to the mode-based default.
208
+ */
209
+ routeParamResolver?: (ctx: {
210
+ parentEntity: string
211
+ parentRouteName: string
212
+ childEntity: string | null
213
+ parentIdField: string
214
+ }) => string | undefined
195
215
  /**
196
216
  * Existing Vue app to install qdadm onto. When provided, the Kernel
197
217
  * skips its own `createApp(WrappedRoot)` and reuses the supplied
@@ -45,6 +45,89 @@ function capitalize(str: string): string {
45
45
  return str.charAt(0).toUpperCase() + str.slice(1)
46
46
  }
47
47
 
48
+ /** Context handed to a kernel-level routeParamResolver (#1201). */
49
+ export interface RouteParamResolverContext {
50
+ parentEntity: string
51
+ parentRouteName: string
52
+ /** Child entity name, or null for a non-entity childPage. */
53
+ childEntity: string | null
54
+ parentIdField: string
55
+ }
56
+
57
+ /**
58
+ * Resolve the URL param name carrying the parent id in a child route
59
+ * family (#1201). Precedence: per-call `parentParam` > kernel
60
+ * `routeParamResolver` > `parentParamMode`:
61
+ *
62
+ * - `auto` (default): families whose routes never carry the child's own
63
+ * id (list-only, childPage) keep the bare parent `idField` param —
64
+ * unchanged URLs. Families with show/edit routes namespace the parent
65
+ * (`:jobId`) so the principal (child) id stays `:id`.
66
+ * - `always`: namespaced parent param everywhere.
67
+ * - `bare`: legacy `idField` param everywhere; a would-be collision
68
+ * throws at registration instead of silently shadowing the parent.
69
+ */
70
+ function resolveParentParam(opts: {
71
+ kernelOptions?: {
72
+ parentParamMode?: 'auto' | 'always' | 'bare'
73
+ routeParamResolver?: (ctx: RouteParamResolverContext) => string | undefined
74
+ }
75
+ explicit?: string
76
+ parentEntity: string
77
+ parentRouteName: string
78
+ childEntity: string | null
79
+ parentIdField: string
80
+ childIdParam: string
81
+ hasOwnIdParam: boolean
82
+ }): string {
83
+ const {
84
+ kernelOptions,
85
+ explicit,
86
+ parentEntity,
87
+ parentRouteName,
88
+ childEntity,
89
+ parentIdField,
90
+ childIdParam,
91
+ hasOwnIdParam,
92
+ } = opts
93
+
94
+ if (explicit) return explicit
95
+
96
+ const resolved = kernelOptions?.routeParamResolver?.({
97
+ parentEntity,
98
+ parentRouteName,
99
+ childEntity,
100
+ parentIdField,
101
+ })
102
+ if (resolved) return resolved
103
+
104
+ const mode = kernelOptions?.parentParamMode || 'auto'
105
+ const namespaced = `${singularize(parentEntity)}Id`
106
+
107
+ if (mode === 'always') return namespaced
108
+
109
+ if (mode === 'bare') {
110
+ if (hasOwnIdParam && parentIdField === childIdParam) {
111
+ throw new Error(
112
+ `[qdadm] crud('${childEntity}', { parentRoute: '${parentRouteName}' }): parent and child ` +
113
+ `both use the URL param ':${childIdParam}' — the child id would shadow the parent id. ` +
114
+ `Switch parentParamMode to 'auto', or set an explicit parentParam.`
115
+ )
116
+ }
117
+ return parentIdField
118
+ }
119
+
120
+ // mode === 'auto'
121
+ if (!hasOwnIdParam) return parentIdField
122
+ if (namespaced === childIdParam) {
123
+ throw new Error(
124
+ `[qdadm] crud('${childEntity}', { parentRoute: '${parentRouteName}' }): the namespaced ` +
125
+ `parent param ':${namespaced}' collides with the child idField — set an explicit parentParam.`
126
+ )
127
+ }
128
+ return namespaced
129
+ }
130
+
48
131
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
132
  export function applyRoutingMethods(KernelContextClass: { prototype: any }): void {
50
133
  const proto = KernelContextClass.prototype as Self
@@ -103,7 +186,20 @@ export function applyRoutingMethods(KernelContextClass: { prototype: any }): voi
103
186
  const parentManager = parentEntityName
104
187
  ? this._kernel.orchestrator?.get(parentEntityName)
105
188
  : null
106
- const parentIdParam = parentManager?.idField || 'id'
189
+ const parentIdField = parentManager?.idField || 'id'
190
+ // Families with show/edit routes carry the child's own :id — the
191
+ // parent param must not collide with it (#1201).
192
+ const hasOwnIdParam = !!(pages.show || pages.form || pages.edit)
193
+ const parentIdParam = resolveParentParam({
194
+ kernelOptions: this._kernel.options,
195
+ explicit: options.parentParam,
196
+ parentEntity: parentEntityName || parentRouteName,
197
+ parentRouteName,
198
+ childEntity: entity,
199
+ parentIdField,
200
+ childIdParam: idParam,
201
+ hasOwnIdParam,
202
+ })
107
203
 
108
204
  // Build base path: parentPath/:parentId/entity (e.g., books/:bookId/loans)
109
205
  const parentBasePath =
@@ -238,7 +334,19 @@ export function applyRoutingMethods(KernelContextClass: { prototype: any }): voi
238
334
  const parentManager = parentEntityName
239
335
  ? this._kernel.orchestrator?.get(parentEntityName)
240
336
  : null
241
- const parentIdParam = parentManager?.idField || 'id'
337
+ const parentIdField = parentManager?.idField || 'id'
338
+ // childPage has no own :id (single-id family) — auto mode keeps the
339
+ // bare param; explicit parentParam / resolver / 'always' still apply.
340
+ const parentIdParam = resolveParentParam({
341
+ kernelOptions: this._kernel.options,
342
+ explicit: options.parentParam,
343
+ parentEntity: parentEntityName || parentRouteName,
344
+ parentRouteName,
345
+ childEntity: null,
346
+ parentIdField,
347
+ childIdParam: 'id',
348
+ hasOwnIdParam: false,
349
+ })
242
350
 
243
351
  const parentBasePath =
244
352
  parentRoute.path.replace(/\/(create|:.*)?$/, '') || parentEntityName
@@ -148,6 +148,12 @@ export interface CrudOptions {
148
148
  parentRoute?: string
149
149
  foreignKey?: string
150
150
  label?: string
151
+ /**
152
+ * Explicit URL param name carrying the parent id in child routes
153
+ * (e.g. 'jobUuid' → `jobs/:jobUuid/tasks/:id`). Overrides the kernel
154
+ * `routeParamResolver` and `parentParamMode` (#1201).
155
+ */
156
+ parentParam?: string
151
157
  }
152
158
 
153
159
  /** Child page options for non-entity child routes */
@@ -156,6 +162,8 @@ export interface ChildPageOptions {
156
162
  label?: string
157
163
  icon?: string
158
164
  meta?: Record<string, unknown>
165
+ /** Explicit URL param name for the parent id (see CrudOptions.parentParam). */
166
+ parentParam?: string
159
167
  }
160
168
 
161
169
  /** User entity options */