velocious 1.0.457 → 1.0.459
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 +2 -0
- package/build/configuration-types.js +18 -2
- package/build/current.js +3 -3
- package/build/database/migration/index.js +2 -0
- package/build/database/record/index.js +1 -1
- package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +76 -9
- package/build/frontend-models/base.js +37 -15
- package/build/frontend-models/resource-definition.js +105 -14
- package/build/src/configuration-types.d.ts +53 -6
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +17 -3
- package/build/src/current.d.ts +6 -6
- package/build/src/current.d.ts.map +1 -1
- package/build/src/current.js +4 -4
- package/build/src/database/migration/index.d.ts +8 -0
- package/build/src/database/migration/index.d.ts.map +1 -1
- package/build/src/database/migration/index.js +3 -1
- package/build/src/database/record/index.d.ts +2 -2
- package/build/src/database/record/index.d.ts.map +1 -1
- package/build/src/database/record/index.js +2 -2
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +35 -0
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +68 -10
- package/build/src/frontend-models/base.d.ts +73 -51
- package/build/src/frontend-models/base.d.ts.map +1 -1
- package/build/src/frontend-models/base.js +38 -16
- package/build/src/frontend-models/resource-definition.d.ts.map +1 -1
- package/build/src/frontend-models/resource-definition.js +92 -15
- package/build/src/testing/test-runner.d.ts +5 -0
- package/build/src/testing/test-runner.d.ts.map +1 -1
- package/build/src/testing/test-runner.js +2 -1
- package/build/testing/test-runner.js +1 -0
- package/package.json +1 -1
- package/src/configuration-types.js +18 -2
- package/src/current.js +3 -3
- package/src/database/migration/index.js +2 -0
- package/src/database/record/index.js +1 -1
- package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +76 -9
- package/src/frontend-models/base.js +37 -15
- package/src/frontend-models/resource-definition.js +105 -14
- package/src/testing/test-runner.js +1 -0
package/README.md
CHANGED
|
@@ -378,6 +378,8 @@ export default new Configuration({
|
|
|
378
378
|
`frontendModels` entries must be `FrontendModelBaseResource` subclasses. Built-in CRUD/find/index/serialize behavior lives in the base class, and app resources override only the pieces they actually need.
|
|
379
379
|
Resource-level index customization should prefer `indexQuery()` or the pagination/search/sort hooks over replacing `records()`, so built-in pluck and aggregate count support can keep using the same query. See [`docs/frontend-model-resources.md`](docs/frontend-model-resources.md) for the resource extension points.
|
|
380
380
|
|
|
381
|
+
Custom class- and instance-level commands are declared via `collectionCommands` / `memberCommands`. Each entry is a plain camelCase method name, or a `{name, args?, returnType?}` object that types the command's arguments and response — e.g. `memberCommands: ["suspend", {name: "refresh", args: [{name: "age", type: "number"}], returnType: "string"}]`. See [`docs/frontend-model-resources.md#custom-commands`](docs/frontend-model-resources.md#custom-commands).
|
|
382
|
+
|
|
381
383
|
Resources expose the full CRUD ability set (`create`, `destroy`, `read`, `update`) by default. To restrict the API surface — for example to a read-only resource — declare an explicit subset:
|
|
382
384
|
|
|
383
385
|
```js
|
|
@@ -304,8 +304,8 @@
|
|
|
304
304
|
* @property {string[]} [abilities] - Ability action list (camelCase action names). Defaults to `["read"]` for `find` and `index` when omitted.
|
|
305
305
|
* @property {Record<string, FrontendModelAttachmentConfiguration>} [attachments] - Attachment helpers keyed by attachment name.
|
|
306
306
|
* @property {string[]} [commands] - Legacy built-in command names (`index`, `find`, `create`, `update`, `destroy`, `attach`, `download`, `url`).
|
|
307
|
-
* @property {
|
|
308
|
-
* @property {
|
|
307
|
+
* @property {Array<FrontendModelResourceCustomCommand>} [collectionCommands] - Custom collection commands. Each entry is a camelCase method name, or a `{name, args?, returnType?}` object declaring typed arguments and/or a response type. The runtime derives the kebab-case command slug from the name.
|
|
308
|
+
* @property {Array<FrontendModelResourceCustomCommand>} [memberCommands] - Custom member commands. Each entry is a camelCase method name, or a `{name, args?, returnType?}` object declaring typed arguments and/or a response type. The runtime derives the kebab-case command slug from the name.
|
|
309
309
|
* @property {string[]} [builtInCollectionCommands] - Built-in collection command names (`index`, `create`).
|
|
310
310
|
* @property {string[]} [builtInMemberCommands] - Built-in member command names (`find`, `update`, `destroy`, `attach`, `download`, `url`).
|
|
311
311
|
* @property {string[]} [relationships] - Relationship names to expose in frontend models. Type and target model are inferred from the backend model's registered relationships.
|
|
@@ -313,12 +313,28 @@
|
|
|
313
313
|
* @property {FrontendModelResourceServerConfiguration} [server] - Optional legacy backend behavior overrides for built-in frontend actions.
|
|
314
314
|
*/
|
|
315
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Object form of a custom command entry, declaring its typed arguments and/or
|
|
318
|
+
* response type alongside the command name.
|
|
319
|
+
* @typedef {object} FrontendModelResourceCustomCommandObject
|
|
320
|
+
* @property {string} name - camelCase command method name.
|
|
321
|
+
* @property {Array<{name: string, type: string}>} [args] - Typed command arguments; each generates a named, typed method parameter mapped positionally into the command payload. `type` is a JSDoc type string.
|
|
322
|
+
* @property {string} [returnType] - JSDoc type for the command response. When set, the generated method is typed `Promise<returnType>` instead of `Promise<Record<string, ?>>`. Emitted verbatim into the generated frontend model, so it must resolve there.
|
|
323
|
+
*/
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* A custom command entry: a plain camelCase method name, or an object declaring
|
|
327
|
+
* typed args and/or a response type.
|
|
328
|
+
* @typedef {string | FrontendModelResourceCustomCommandObject} FrontendModelResourceCustomCommand
|
|
329
|
+
*/
|
|
330
|
+
|
|
316
331
|
/**
|
|
317
332
|
* @typedef {Omit<FrontendModelResourceConfiguration, "abilities" | "builtInCollectionCommands" | "builtInMemberCommands" | "collectionCommands" | "commands" | "memberCommands"> & {
|
|
318
333
|
* abilities: FrontendModelResourceAbilitiesConfiguration
|
|
319
334
|
* builtInCollectionCommands: Record<string, string>
|
|
320
335
|
* builtInMemberCommands: Record<string, string>
|
|
321
336
|
* collectionCommands: Record<string, string>
|
|
337
|
+
* commandMetadata: Record<string, {args: Array<{name: string, type: string}>, returnType: string | null}>
|
|
322
338
|
* memberCommands: Record<string, string>
|
|
323
339
|
* }} NormalizedFrontendModelResourceConfiguration
|
|
324
340
|
*/
|
package/build/current.js
CHANGED
|
@@ -47,7 +47,7 @@ export default class Current {
|
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* Runs tenant.
|
|
50
|
-
* @returns {
|
|
50
|
+
* @returns {Record<string, unknown> | undefined} - Current tenant.
|
|
51
51
|
*/
|
|
52
52
|
static tenant() {
|
|
53
53
|
try {
|
|
@@ -61,7 +61,7 @@ export default class Current {
|
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* Runs set tenant.
|
|
64
|
-
* @param {
|
|
64
|
+
* @param {Record<string, unknown>} tenant - Tenant.
|
|
65
65
|
* @returns {void} - No return value.
|
|
66
66
|
*/
|
|
67
67
|
static setTenant(tenant) {
|
|
@@ -70,7 +70,7 @@ export default class Current {
|
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
72
|
* Runs with tenant.
|
|
73
|
-
* @param {
|
|
73
|
+
* @param {Record<string, unknown>} tenant - Tenant.
|
|
74
74
|
* @param {() => Promise<?>} callback - Callback.
|
|
75
75
|
* @returns {Promise<?>} - Callback result.
|
|
76
76
|
*/
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* @property {?} [default] - Default value for the column.
|
|
7
7
|
* @property {object} [foreignKey] - Foreign key definition for the column.
|
|
8
8
|
* @property {boolean | {unique: boolean}} [index] - Whether to add an index (optionally unique).
|
|
9
|
+
* @property {number} [limit] - Alias for maxLength (varchar length limit) on string-like columns.
|
|
10
|
+
* @property {number} [maxLength] - Maximum length for string-like columns (e.g. varchar length).
|
|
9
11
|
* @property {boolean} [null] - Whether the column allows null values.
|
|
10
12
|
* @property {boolean} [primaryKey] - Whether the column is a primary key.
|
|
11
13
|
* @property {boolean} [unique] - Whether the column enforces uniqueness.
|
|
@@ -1678,7 +1678,7 @@ class VelociousDatabaseRecord {
|
|
|
1678
1678
|
|
|
1679
1679
|
/**
|
|
1680
1680
|
* Declares a tenant-aware database identifier resolver for this model class.
|
|
1681
|
-
* @param {string | ((args: {modelClass: typeof VelociousDatabaseRecord, tenant:
|
|
1681
|
+
* @param {string | ((args: {modelClass: typeof VelociousDatabaseRecord, tenant: Record<string, unknown>}) => string | undefined)} databaseIdentifierOrResolver - Static identifier or resolver.
|
|
1682
1682
|
* @returns {void} - No return value.
|
|
1683
1683
|
*/
|
|
1684
1684
|
static switchesTenantDatabase(databaseIdentifierOrResolver) {
|
|
@@ -288,6 +288,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
288
288
|
}
|
|
289
289
|
const collectionCommands = modelConfig.collectionCommands
|
|
290
290
|
const memberCommands = modelConfig.memberCommands
|
|
291
|
+
const commandMetadata = modelConfig.commandMetadata || {}
|
|
291
292
|
const builtInCollectionCommandsAreDefault = builtInCollectionCommands.create === "create" && builtInCollectionCommands.index === "index"
|
|
292
293
|
const builtInMemberCommandsAreDefault = builtInMemberCommands.attach === "attach"
|
|
293
294
|
&& builtInMemberCommands.destroy === "destroy"
|
|
@@ -460,35 +461,39 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
460
461
|
}
|
|
461
462
|
|
|
462
463
|
for (const methodName of Object.keys(collectionCommands)) {
|
|
464
|
+
const signature = this.customCommandMethodSignature({commandMetadata, methodName})
|
|
465
|
+
|
|
463
466
|
fileContent += "\n"
|
|
464
467
|
fileContent += " /**\n"
|
|
465
468
|
fileContent += ` * Runs ${methodName}.\n`
|
|
466
|
-
fileContent +=
|
|
467
|
-
fileContent +=
|
|
469
|
+
fileContent += signature.paramDocs
|
|
470
|
+
fileContent += ` * @returns {Promise<${signature.returnType}>} - Command response.\n`
|
|
468
471
|
fileContent += " */\n"
|
|
469
|
-
fileContent += ` static async ${methodName}(
|
|
472
|
+
fileContent += ` static async ${methodName}(${signature.parameters}) {\n`
|
|
470
473
|
fileContent += " return await this.executeCustomCommand({\n"
|
|
471
474
|
fileContent += ` commandName: ${JSON.stringify(collectionCommands[methodName])},\n`
|
|
472
475
|
fileContent += ` commandType: ${JSON.stringify(collectionCommands[methodName])},\n`
|
|
473
|
-
fileContent += ` payload: ${className}.normalizeCustomCommandPayloadArguments(
|
|
476
|
+
fileContent += ` payload: ${className}.normalizeCustomCommandPayloadArguments(${signature.payloadArguments}),\n`
|
|
474
477
|
fileContent += " resourcePath: this.resourcePath()\n"
|
|
475
478
|
fileContent += " })\n"
|
|
476
479
|
fileContent += " }\n"
|
|
477
480
|
}
|
|
478
481
|
|
|
479
482
|
for (const methodName of Object.keys(memberCommands)) {
|
|
483
|
+
const signature = this.customCommandMethodSignature({commandMetadata, methodName})
|
|
484
|
+
|
|
480
485
|
fileContent += "\n"
|
|
481
486
|
fileContent += " /**\n"
|
|
482
487
|
fileContent += ` * Runs ${methodName}.\n`
|
|
483
|
-
fileContent +=
|
|
484
|
-
fileContent +=
|
|
488
|
+
fileContent += signature.paramDocs
|
|
489
|
+
fileContent += ` * @returns {Promise<${signature.returnType}>} - Command response.\n`
|
|
485
490
|
fileContent += " */\n"
|
|
486
|
-
fileContent += ` async ${methodName}(
|
|
491
|
+
fileContent += ` async ${methodName}(${signature.parameters}) {\n`
|
|
487
492
|
fileContent += ` return await ${className}.executeCustomCommand({\n`
|
|
488
493
|
fileContent += ` commandName: ${JSON.stringify(memberCommands[methodName])},\n`
|
|
489
494
|
fileContent += ` commandType: ${JSON.stringify(memberCommands[methodName])},\n`
|
|
490
495
|
fileContent += " memberId: this.primaryKeyValue(),\n"
|
|
491
|
-
fileContent += ` payload: ${className}.normalizeCustomCommandPayloadArguments(
|
|
496
|
+
fileContent += ` payload: ${className}.normalizeCustomCommandPayloadArguments(${signature.payloadArguments}),\n`
|
|
492
497
|
fileContent += ` resourcePath: ${className}.resourcePath()\n`
|
|
493
498
|
fileContent += " })\n"
|
|
494
499
|
fileContent += " }\n"
|
|
@@ -1327,7 +1332,69 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
1327
1332
|
sourceClassName: ownerClassName
|
|
1328
1333
|
})
|
|
1329
1334
|
|
|
1330
|
-
|
|
1335
|
+
// Frontend attributes hold the serialized (resolved) value, so an async
|
|
1336
|
+
// backend accessor typed `Promise<number>` must surface as `number` — the
|
|
1337
|
+
// same unwrapping the resource-method inference path applies.
|
|
1338
|
+
return jsDocType
|
|
1339
|
+
? {jsDocType: this.frontendResolvableAttributeJsDocType(this.unwrappedPromiseJsDocType({jsDocType}))}
|
|
1340
|
+
: null
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/**
|
|
1344
|
+
* A backend accessor's `@returns` can reference types that exist only on the
|
|
1345
|
+
* backend (e.g. a model-local `@typedef AgentRunPlanningArtifact`). The frontend
|
|
1346
|
+
* model can't resolve those, so fall back to `any` rather than emitting an
|
|
1347
|
+
* undefined type name. Types built only from primitives and known generic
|
|
1348
|
+
* builtins pass through unchanged.
|
|
1349
|
+
* @param {string} jsDocType - Resolved (Promise-unwrapped) attribute type.
|
|
1350
|
+
* @returns {string} - A frontend-resolvable attribute type.
|
|
1351
|
+
*/
|
|
1352
|
+
frontendResolvableAttributeJsDocType(jsDocType) {
|
|
1353
|
+
const safeTypeIdentifiers = new Set([
|
|
1354
|
+
"Array", "Date", "Exclude", "Extract", "FrontendModelAttributeValue", "FrontendModelTransportValue",
|
|
1355
|
+
"Map", "NonNullable", "Omit", "Partial", "Pick", "Promise", "Readonly", "ReadonlyArray", "Record",
|
|
1356
|
+
"Required", "ReturnType", "Set"
|
|
1357
|
+
])
|
|
1358
|
+
const referencedIdentifiers = jsDocType.match(/[A-Z][A-Za-z0-9_$]*/g) || []
|
|
1359
|
+
|
|
1360
|
+
if (referencedIdentifiers.some((identifier) => !safeTypeIdentifiers.has(identifier))) {
|
|
1361
|
+
return "any"
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
return jsDocType
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/**
|
|
1368
|
+
* Builds the JSDoc param block, parameter list, payload-argument expression, and
|
|
1369
|
+
* return type for a custom command method. With declared `args` each becomes a
|
|
1370
|
+
* named, typed parameter mapped positionally into the command payload; without
|
|
1371
|
+
* them the method stays variadic.
|
|
1372
|
+
* @param {object} args - Arguments.
|
|
1373
|
+
* @param {Record<string, {args: Array<{name: string, type: string}>, returnType: string | null}>} args.commandMetadata - Per-command metadata.
|
|
1374
|
+
* @param {string} args.methodName - Command method name.
|
|
1375
|
+
* @returns {{paramDocs: string, parameters: string, payloadArguments: string, returnType: string}} - Generation pieces.
|
|
1376
|
+
*/
|
|
1377
|
+
customCommandMethodSignature({commandMetadata, methodName}) {
|
|
1378
|
+
const metadata = commandMetadata[methodName] || {args: [], returnType: null}
|
|
1379
|
+
const returnType = metadata.returnType || "Record<string, ?>"
|
|
1380
|
+
|
|
1381
|
+
if (metadata.args.length > 0) {
|
|
1382
|
+
const parameterNames = metadata.args.map((arg) => arg.name)
|
|
1383
|
+
|
|
1384
|
+
return {
|
|
1385
|
+
paramDocs: metadata.args.map((arg) => ` * @param {${arg.type}} ${arg.name} - Command argument.\n`).join(""),
|
|
1386
|
+
parameters: parameterNames.join(", "),
|
|
1387
|
+
payloadArguments: `[${parameterNames.join(", ")}]`,
|
|
1388
|
+
returnType
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
return {
|
|
1393
|
+
paramDocs: " * @param {...FrontendModelAttributeValue} commandArguments - Custom command arguments.\n",
|
|
1394
|
+
parameters: "...commandArguments",
|
|
1395
|
+
payloadArguments: "commandArguments",
|
|
1396
|
+
returnType
|
|
1397
|
+
}
|
|
1331
1398
|
}
|
|
1332
1399
|
|
|
1333
1400
|
/**
|
|
@@ -66,9 +66,19 @@ import {readPayloadAssociationCount, readPayloadComputedAbility, readPayloadQuer
|
|
|
66
66
|
*/
|
|
67
67
|
/**
|
|
68
68
|
* Frontend model static side.
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
69
|
+
*
|
|
70
|
+
* The template defaults are intentionally permissive (`any` model/attribute
|
|
71
|
+
* params). The bare `FrontendModelClass` is the `@this`/constraint type on the
|
|
72
|
+
* static query methods (findBy/find/where/preload/...); a generated subclass
|
|
73
|
+
* declares typed-attribute generics (e.g. `FrontendModelBase<AccountAttributes,
|
|
74
|
+
* AccountCreateAttributes, AccountUpdateAttributes>`) which, against a concrete
|
|
75
|
+
* `Record<string, FrontendModelTransportValue>` default, fail the constraint by
|
|
76
|
+
* invariance. Defaulting to `any` lets any subclass satisfy the constraint while
|
|
77
|
+
* the methods' own `@template T` still captures the precise calling class for
|
|
78
|
+
* their return types.
|
|
79
|
+
* @template {FrontendModelBase} [T=FrontendModelBase<any, any, any>]
|
|
80
|
+
* @template {object} [Attributes=any]
|
|
81
|
+
* @template {object} [CreateAttributes=any]
|
|
72
82
|
* @typedef {{new (): T, create(attributes?: CreateAttributes): Promise<T>} & Omit<typeof FrontendModelBase, "create" | "prototype">} FrontendModelClass
|
|
73
83
|
*/
|
|
74
84
|
/**
|
|
@@ -80,7 +90,7 @@ import {readPayloadAssociationCount, readPayloadComputedAbility, readPayloadQuer
|
|
|
80
90
|
* Loaded instance type for relationship helper generics. Older generated
|
|
81
91
|
* frontend models passed model classes into relationship helpers, while newer
|
|
82
92
|
* generated models pass instance types.
|
|
83
|
-
* @template {FrontendModelBase | typeof FrontendModelBase} T
|
|
93
|
+
* @template {FrontendModelBase<any, any, any> | typeof FrontendModelBase} T
|
|
84
94
|
* @typedef {T extends new (...args: any[]) => infer Instance ? Instance : T} FrontendModelRelationshipModel
|
|
85
95
|
*/
|
|
86
96
|
/**
|
|
@@ -281,9 +291,9 @@ export class AttributeNotSelectedError extends Error {
|
|
|
281
291
|
|
|
282
292
|
/**
|
|
283
293
|
* Lightweight singular relationship state holder for frontend model instances.
|
|
284
|
-
* @template {FrontendModelBase | typeof FrontendModelBase} S
|
|
285
|
-
* @template {FrontendModelBase | typeof FrontendModelBase} T
|
|
286
|
-
* @template {
|
|
294
|
+
* @template {FrontendModelBase<any, any, any> | typeof FrontendModelBase} S
|
|
295
|
+
* @template {FrontendModelBase<any, any, any> | typeof FrontendModelBase} T
|
|
296
|
+
* @template {object} [TargetCreateAttributes=Record<string, FrontendModelAttributeValue>]
|
|
287
297
|
*/
|
|
288
298
|
export class FrontendModelSingularRelationship {
|
|
289
299
|
/**
|
|
@@ -401,9 +411,9 @@ export class FrontendModelSingularRelationship {
|
|
|
401
411
|
|
|
402
412
|
/**
|
|
403
413
|
* Lightweight has-many relationship state holder for frontend model instances.
|
|
404
|
-
* @template {FrontendModelBase | typeof FrontendModelBase} S
|
|
405
|
-
* @template {FrontendModelBase | typeof FrontendModelBase} T
|
|
406
|
-
* @template {
|
|
414
|
+
* @template {FrontendModelBase<any, any, any> | typeof FrontendModelBase} S
|
|
415
|
+
* @template {FrontendModelBase<any, any, any> | typeof FrontendModelBase} T
|
|
416
|
+
* @template {object} [TargetCreateAttributes=Record<string, FrontendModelAttributeValue>]
|
|
407
417
|
*/
|
|
408
418
|
export class FrontendModelHasManyRelationship {
|
|
409
419
|
/**
|
|
@@ -540,8 +550,13 @@ export class FrontendModelHasManyRelationship {
|
|
|
540
550
|
}
|
|
541
551
|
|
|
542
552
|
/**
|
|
543
|
-
* Frontend model relationship helper type.
|
|
544
|
-
*
|
|
553
|
+
* Frontend model relationship helper type. Returned by `getRelationshipByName`,
|
|
554
|
+
* which generated models immediately cast to their concrete relationship type
|
|
555
|
+
* (e.g. `FrontendModelSingularRelationship<Owner, Target, TargetCreateAttributes>`).
|
|
556
|
+
* The members use `any` type args so that cast is allowed regardless of the
|
|
557
|
+
* target model's typed-attribute generics — a concrete `FrontendModelBase` member
|
|
558
|
+
* here makes the cast a non-overlapping (TS2352) error for every typed model.
|
|
559
|
+
* @typedef {FrontendModelHasManyRelationship<any, any, any> | FrontendModelSingularRelationship<any, any, any>} FrontendModelRelationship
|
|
545
560
|
*/
|
|
546
561
|
|
|
547
562
|
/**
|
|
@@ -1883,9 +1898,16 @@ function assertDefinedFindByConditionValue(value, keyPath) {
|
|
|
1883
1898
|
|
|
1884
1899
|
/**
|
|
1885
1900
|
* Base frontend model.
|
|
1886
|
-
*
|
|
1887
|
-
*
|
|
1888
|
-
*
|
|
1901
|
+
*
|
|
1902
|
+
* Defaults are `any` so the bare `FrontendModelBase` — used throughout as a
|
|
1903
|
+
* constraint/parameter type for "any frontend model" — accepts generated
|
|
1904
|
+
* subclasses declaring typed-attribute generics (`FrontendModelBase<XAttributes,
|
|
1905
|
+
* ...>`). A concrete `Record<string, FrontendModelAttributeValue>` default makes
|
|
1906
|
+
* those subclasses fail by invariance. Subclasses still pass their precise
|
|
1907
|
+
* attribute typedefs, so typed accessors keep their precision.
|
|
1908
|
+
* @template {object} [Attributes=any]
|
|
1909
|
+
* @template {object} [CreateAttributes=any]
|
|
1910
|
+
* @template {object} [UpdateAttributes=any]
|
|
1889
1911
|
*/
|
|
1890
1912
|
export default class FrontendModelBase {
|
|
1891
1913
|
/**
|
|
@@ -90,6 +90,10 @@ function normalizeFrontendModelResourceConfiguration(resourceConfiguration) {
|
|
|
90
90
|
builtInCollectionCommands: normalizedCommands.builtInCollectionCommands,
|
|
91
91
|
builtInMemberCommands: normalizedCommands.builtInMemberCommands,
|
|
92
92
|
collectionCommands: normalizedCommands.collectionCommands,
|
|
93
|
+
// Per-command metadata (typed args + declared return type) keyed by method
|
|
94
|
+
// name, derived from `{name, args?, returnType?}` command entries. The
|
|
95
|
+
// generator uses it to type each custom command method.
|
|
96
|
+
commandMetadata: normalizedCommands.commandMetadata,
|
|
93
97
|
memberCommands: normalizedCommands.memberCommands
|
|
94
98
|
}
|
|
95
99
|
}
|
|
@@ -151,7 +155,7 @@ function defaultCrudAbilities() {
|
|
|
151
155
|
/**
|
|
152
156
|
* Runs normalize frontend model resource commands.
|
|
153
157
|
* @param {import("../configuration-types.js").FrontendModelResourceConfiguration} resourceConfiguration - Raw resource configuration.
|
|
154
|
-
* @returns {{builtInCollectionCommands: Record<string, string>, builtInMemberCommands: Record<string, string>, collectionCommands: Record<string, string>, memberCommands: Record<string, string>}} - Normalized command configuration.
|
|
158
|
+
* @returns {{builtInCollectionCommands: Record<string, string>, builtInMemberCommands: Record<string, string>, collectionCommands: Record<string, string>, commandMetadata: Record<string, {args: Array<{name: string, type: string}>, returnType: string | null}>, memberCommands: Record<string, string>}} - Normalized command configuration.
|
|
155
159
|
*/
|
|
156
160
|
function normalizeFrontendModelResourceCommands(resourceConfiguration) {
|
|
157
161
|
const builtInCollectionCommands = resourceConfiguration.builtInCollectionCommands
|
|
@@ -180,11 +184,15 @@ function normalizeFrontendModelResourceCommands(resourceConfiguration) {
|
|
|
180
184
|
modelName: "MemberCommand"
|
|
181
185
|
})
|
|
182
186
|
|
|
187
|
+
const normalizedCollectionCommands = normalizeFrontendModelCustomCommands({commandsConfig: customCollectionCommands, modelName: "CollectionCommand"})
|
|
188
|
+
const normalizedMemberCommands = normalizeFrontendModelCustomCommands({commandsConfig: customMemberCommands, modelName: "MemberCommand"})
|
|
189
|
+
|
|
183
190
|
return {
|
|
184
191
|
builtInCollectionCommands: normalizedBuiltInCollectionCommands,
|
|
185
192
|
builtInMemberCommands: normalizedBuiltInMemberCommands,
|
|
186
|
-
collectionCommands:
|
|
187
|
-
|
|
193
|
+
collectionCommands: normalizedCollectionCommands.commands,
|
|
194
|
+
commandMetadata: {...normalizedCollectionCommands.metadata, ...normalizedMemberCommands.metadata},
|
|
195
|
+
memberCommands: normalizedMemberCommands.commands
|
|
188
196
|
}
|
|
189
197
|
}
|
|
190
198
|
|
|
@@ -228,27 +236,30 @@ function normalizeFrontendModelBuiltInCommands({commandDefaults, commandsConfig,
|
|
|
228
236
|
}
|
|
229
237
|
|
|
230
238
|
/**
|
|
231
|
-
* Runs normalize frontend model custom commands.
|
|
239
|
+
* Runs normalize frontend model custom commands. Entries are either a plain
|
|
240
|
+
* camelCase method-name string or a `{name, args?, returnType?}` object that
|
|
241
|
+
* also declares the command's typed arguments and/or response type.
|
|
232
242
|
* @param {object} args - Arguments.
|
|
233
|
-
* @param {string
|
|
243
|
+
* @param {Array<string | {name: string, args?: Array<{name: string, type: string}>, returnType?: string}> | undefined} args.commandsConfig - Custom commands config.
|
|
234
244
|
* @param {string} args.modelName - Diagnostic model name.
|
|
235
|
-
* @returns {Record<string, string>} -
|
|
245
|
+
* @returns {{commands: Record<string, string>, metadata: Record<string, {args: Array<{name: string, type: string}>, returnType: string | null}>}} - Route map (method name → kebab slug) + per-command metadata.
|
|
236
246
|
*/
|
|
237
247
|
function normalizeFrontendModelCustomCommands({commandsConfig, modelName}) {
|
|
238
248
|
if (!commandsConfig) {
|
|
239
|
-
return {}
|
|
249
|
+
return {commands: {}, metadata: {}}
|
|
240
250
|
}
|
|
241
251
|
|
|
242
252
|
if (!Array.isArray(commandsConfig)) {
|
|
243
253
|
throw new Error(`${modelName} configuration must use the array form. Object form is no longer supported.`)
|
|
244
254
|
}
|
|
245
255
|
|
|
246
|
-
/**
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const
|
|
256
|
+
/** @type {Record<string, string>} */
|
|
257
|
+
const commands = {}
|
|
258
|
+
/** @type {Record<string, {args: Array<{name: string, type: string}>, returnType: string | null}>} */
|
|
259
|
+
const metadata = {}
|
|
250
260
|
|
|
251
|
-
for (const
|
|
261
|
+
for (const commandEntry of commandsConfig) {
|
|
262
|
+
const {methodName, args, returnType} = normalizeFrontendModelCustomCommandEntry({commandEntry, modelName})
|
|
252
263
|
const validatedMethodName = validateFrontendModelResourceCommandName({
|
|
253
264
|
commandName: methodName,
|
|
254
265
|
commandType: methodName,
|
|
@@ -256,10 +267,90 @@ function normalizeFrontendModelCustomCommands({commandsConfig, modelName}) {
|
|
|
256
267
|
})
|
|
257
268
|
const commandSlug = inflection.dasherize(inflection.underscore(validatedMethodName))
|
|
258
269
|
|
|
259
|
-
|
|
270
|
+
commands[validatedMethodName] = commandSlug
|
|
271
|
+
metadata[validatedMethodName] = {args, returnType}
|
|
260
272
|
}
|
|
261
273
|
|
|
262
|
-
return
|
|
274
|
+
return {commands, metadata}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Normalizes one custom-command entry (string shorthand or contract object).
|
|
279
|
+
* @param {object} args - Arguments.
|
|
280
|
+
* @param {unknown} args.commandEntry - Raw command entry.
|
|
281
|
+
* @param {string} args.modelName - Diagnostic model name.
|
|
282
|
+
* @returns {{methodName: string, args: Array<{name: string, type: string}>, returnType: string | null}} - Method name + metadata.
|
|
283
|
+
*/
|
|
284
|
+
function normalizeFrontendModelCustomCommandEntry({commandEntry, modelName}) {
|
|
285
|
+
if (typeof commandEntry === "string") {
|
|
286
|
+
return {methodName: commandEntry, args: [], returnType: null}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (!commandEntry || typeof commandEntry !== "object" || Array.isArray(commandEntry)) {
|
|
290
|
+
throw new Error(`${modelName} entries must be a camelCase name string or a {name, args?, returnType?} object`)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const {name, args, returnType, ...rest} = /** @type {{name?: unknown, args?: unknown, returnType?: unknown}} */ (commandEntry)
|
|
294
|
+
|
|
295
|
+
if (Object.keys(rest).length > 0) {
|
|
296
|
+
throw new Error(`Unexpected ${modelName} keys: ${Object.keys(rest).join(", ")}. Allowed: name, args, returnType`)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (typeof name !== "string" || name.length < 1) {
|
|
300
|
+
throw new Error(`${modelName} object entries require a non-empty 'name' string`)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
methodName: name,
|
|
305
|
+
args: normalizeFrontendModelCommandArgs({args, commandName: name, modelName}),
|
|
306
|
+
returnType: normalizeFrontendModelCommandReturnType({commandName: name, modelName, returnType})
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Validates and normalizes a custom command's typed-argument list.
|
|
312
|
+
* @param {object} args - Arguments.
|
|
313
|
+
* @param {unknown} args.args - Raw command args.
|
|
314
|
+
* @param {string} args.commandName - Command name for diagnostics.
|
|
315
|
+
* @param {string} args.modelName - Diagnostic model name.
|
|
316
|
+
* @returns {Array<{name: string, type: string}>} - Normalized typed command arguments.
|
|
317
|
+
*/
|
|
318
|
+
function normalizeFrontendModelCommandArgs({args, commandName, modelName}) {
|
|
319
|
+
if (args === undefined || args === null) {
|
|
320
|
+
return []
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (!Array.isArray(args)) {
|
|
324
|
+
throw new Error(`${modelName} '${commandName}' args must be an array of {name, type} objects`)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return args.map((arg) => {
|
|
328
|
+
if (!arg || typeof arg !== "object" || typeof arg.name !== "string" || arg.name.length < 1 || typeof arg.type !== "string" || arg.type.trim().length < 1) {
|
|
329
|
+
throw new Error(`${modelName} '${commandName}' args entries require non-empty 'name' and JSDoc-type 'type' strings`)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return {name: arg.name, type: arg.type.trim()}
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Validates and normalizes a custom command's declared JSDoc return type.
|
|
338
|
+
* @param {object} args - Arguments.
|
|
339
|
+
* @param {string} args.commandName - Command name for diagnostics.
|
|
340
|
+
* @param {string} args.modelName - Diagnostic model name.
|
|
341
|
+
* @param {unknown} args.returnType - Raw return type.
|
|
342
|
+
* @returns {string | null} - Normalized JSDoc return type.
|
|
343
|
+
*/
|
|
344
|
+
function normalizeFrontendModelCommandReturnType({commandName, modelName, returnType}) {
|
|
345
|
+
if (returnType === undefined || returnType === null) {
|
|
346
|
+
return null
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (typeof returnType !== "string" || returnType.trim().length < 1) {
|
|
350
|
+
throw new Error(`${modelName} '${commandName}' returnType must be a non-empty JSDoc type string`)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return returnType.trim()
|
|
263
354
|
}
|
|
264
355
|
|
|
265
356
|
/**
|
|
@@ -264,20 +264,34 @@
|
|
|
264
264
|
* @property {string[]} [abilities] - Ability action list (camelCase action names). Defaults to `["read"]` for `find` and `index` when omitted.
|
|
265
265
|
* @property {Record<string, FrontendModelAttachmentConfiguration>} [attachments] - Attachment helpers keyed by attachment name.
|
|
266
266
|
* @property {string[]} [commands] - Legacy built-in command names (`index`, `find`, `create`, `update`, `destroy`, `attach`, `download`, `url`).
|
|
267
|
-
* @property {
|
|
268
|
-
* @property {
|
|
267
|
+
* @property {Array<FrontendModelResourceCustomCommand>} [collectionCommands] - Custom collection commands. Each entry is a camelCase method name, or a `{name, args?, returnType?}` object declaring typed arguments and/or a response type. The runtime derives the kebab-case command slug from the name.
|
|
268
|
+
* @property {Array<FrontendModelResourceCustomCommand>} [memberCommands] - Custom member commands. Each entry is a camelCase method name, or a `{name, args?, returnType?}` object declaring typed arguments and/or a response type. The runtime derives the kebab-case command slug from the name.
|
|
269
269
|
* @property {string[]} [builtInCollectionCommands] - Built-in collection command names (`index`, `create`).
|
|
270
270
|
* @property {string[]} [builtInMemberCommands] - Built-in member command names (`find`, `update`, `destroy`, `attach`, `download`, `url`).
|
|
271
271
|
* @property {string[]} [relationships] - Relationship names to expose in frontend models. Type and target model are inferred from the backend model's registered relationships.
|
|
272
272
|
* @property {string} [primaryKey] - Primary key attribute name.
|
|
273
273
|
* @property {FrontendModelResourceServerConfiguration} [server] - Optional legacy backend behavior overrides for built-in frontend actions.
|
|
274
274
|
*/
|
|
275
|
+
/**
|
|
276
|
+
* Object form of a custom command entry, declaring its typed arguments and/or
|
|
277
|
+
* response type alongside the command name.
|
|
278
|
+
* @typedef {object} FrontendModelResourceCustomCommandObject
|
|
279
|
+
* @property {string} name - camelCase command method name.
|
|
280
|
+
* @property {Array<{name: string, type: string}>} [args] - Typed command arguments; each generates a named, typed method parameter mapped positionally into the command payload. `type` is a JSDoc type string.
|
|
281
|
+
* @property {string} [returnType] - JSDoc type for the command response. When set, the generated method is typed `Promise<returnType>` instead of `Promise<Record<string, ?>>`. Emitted verbatim into the generated frontend model, so it must resolve there.
|
|
282
|
+
*/
|
|
283
|
+
/**
|
|
284
|
+
* A custom command entry: a plain camelCase method name, or an object declaring
|
|
285
|
+
* typed args and/or a response type.
|
|
286
|
+
* @typedef {string | FrontendModelResourceCustomCommandObject} FrontendModelResourceCustomCommand
|
|
287
|
+
*/
|
|
275
288
|
/**
|
|
276
289
|
* @typedef {Omit<FrontendModelResourceConfiguration, "abilities" | "builtInCollectionCommands" | "builtInMemberCommands" | "collectionCommands" | "commands" | "memberCommands"> & {
|
|
277
290
|
* abilities: FrontendModelResourceAbilitiesConfiguration
|
|
278
291
|
* builtInCollectionCommands: Record<string, string>
|
|
279
292
|
* builtInMemberCommands: Record<string, string>
|
|
280
293
|
* collectionCommands: Record<string, string>
|
|
294
|
+
* commandMetadata: Record<string, {args: Array<{name: string, type: string}>, returnType: string | null}>
|
|
281
295
|
* memberCommands: Record<string, string>
|
|
282
296
|
* }} NormalizedFrontendModelResourceConfiguration
|
|
283
297
|
*/
|
|
@@ -1043,13 +1057,13 @@ export type FrontendModelResourceConfiguration = {
|
|
|
1043
1057
|
*/
|
|
1044
1058
|
commands?: string[] | undefined;
|
|
1045
1059
|
/**
|
|
1046
|
-
* - Custom collection commands
|
|
1060
|
+
* - Custom collection commands. Each entry is a camelCase method name, or a `{name, args?, returnType?}` object declaring typed arguments and/or a response type. The runtime derives the kebab-case command slug from the name.
|
|
1047
1061
|
*/
|
|
1048
|
-
collectionCommands?:
|
|
1062
|
+
collectionCommands?: FrontendModelResourceCustomCommand[] | undefined;
|
|
1049
1063
|
/**
|
|
1050
|
-
* - Custom member commands
|
|
1064
|
+
* - Custom member commands. Each entry is a camelCase method name, or a `{name, args?, returnType?}` object declaring typed arguments and/or a response type. The runtime derives the kebab-case command slug from the name.
|
|
1051
1065
|
*/
|
|
1052
|
-
memberCommands?:
|
|
1066
|
+
memberCommands?: FrontendModelResourceCustomCommand[] | undefined;
|
|
1053
1067
|
/**
|
|
1054
1068
|
* - Built-in collection command names (`index`, `create`).
|
|
1055
1069
|
*/
|
|
@@ -1071,11 +1085,44 @@ export type FrontendModelResourceConfiguration = {
|
|
|
1071
1085
|
*/
|
|
1072
1086
|
server?: FrontendModelResourceServerConfiguration | undefined;
|
|
1073
1087
|
};
|
|
1088
|
+
/**
|
|
1089
|
+
* Object form of a custom command entry, declaring its typed arguments and/or
|
|
1090
|
+
* response type alongside the command name.
|
|
1091
|
+
*/
|
|
1092
|
+
export type FrontendModelResourceCustomCommandObject = {
|
|
1093
|
+
/**
|
|
1094
|
+
* - camelCase command method name.
|
|
1095
|
+
*/
|
|
1096
|
+
name: string;
|
|
1097
|
+
/**
|
|
1098
|
+
* - Typed command arguments; each generates a named, typed method parameter mapped positionally into the command payload. `type` is a JSDoc type string.
|
|
1099
|
+
*/
|
|
1100
|
+
args?: {
|
|
1101
|
+
name: string;
|
|
1102
|
+
type: string;
|
|
1103
|
+
}[] | undefined;
|
|
1104
|
+
/**
|
|
1105
|
+
* - JSDoc type for the command response. When set, the generated method is typed `Promise<returnType>` instead of `Promise<Record<string, ?>>`. Emitted verbatim into the generated frontend model, so it must resolve there.
|
|
1106
|
+
*/
|
|
1107
|
+
returnType?: string | undefined;
|
|
1108
|
+
};
|
|
1109
|
+
/**
|
|
1110
|
+
* A custom command entry: a plain camelCase method name, or an object declaring
|
|
1111
|
+
* typed args and/or a response type.
|
|
1112
|
+
*/
|
|
1113
|
+
export type FrontendModelResourceCustomCommand = string | FrontendModelResourceCustomCommandObject;
|
|
1074
1114
|
export type NormalizedFrontendModelResourceConfiguration = Omit<FrontendModelResourceConfiguration, "abilities" | "builtInCollectionCommands" | "builtInMemberCommands" | "collectionCommands" | "commands" | "memberCommands"> & {
|
|
1075
1115
|
abilities: FrontendModelResourceAbilitiesConfiguration;
|
|
1076
1116
|
builtInCollectionCommands: Record<string, string>;
|
|
1077
1117
|
builtInMemberCommands: Record<string, string>;
|
|
1078
1118
|
collectionCommands: Record<string, string>;
|
|
1119
|
+
commandMetadata: Record<string, {
|
|
1120
|
+
args: Array<{
|
|
1121
|
+
name: string;
|
|
1122
|
+
type: string;
|
|
1123
|
+
}>;
|
|
1124
|
+
returnType: string | null;
|
|
1125
|
+
}>;
|
|
1079
1126
|
memberCommands: Record<string, string>;
|
|
1080
1127
|
};
|
|
1081
1128
|
export type FrontendModelResourceClassType = Omit<typeof import("./frontend-model-resource/base-resource.js").default, never> & {
|