@omnifyjp/omnify 3.22.1 → 3.23.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": "@omnifyjp/omnify",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.23.0",
|
|
4
4
|
"description": "Schema-driven code generation for Laravel, TypeScript, and SQL",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"zod": "^3.24.0"
|
|
37
37
|
},
|
|
38
38
|
"optionalDependencies": {
|
|
39
|
-
"@omnifyjp/omnify-darwin-arm64": "3.
|
|
40
|
-
"@omnifyjp/omnify-darwin-x64": "3.
|
|
41
|
-
"@omnifyjp/omnify-linux-x64": "3.
|
|
42
|
-
"@omnifyjp/omnify-linux-arm64": "3.
|
|
43
|
-
"@omnifyjp/omnify-win32-x64": "3.
|
|
39
|
+
"@omnifyjp/omnify-darwin-arm64": "3.23.0",
|
|
40
|
+
"@omnifyjp/omnify-darwin-x64": "3.23.0",
|
|
41
|
+
"@omnifyjp/omnify-linux-x64": "3.23.0",
|
|
42
|
+
"@omnifyjp/omnify-linux-arm64": "3.23.0",
|
|
43
|
+
"@omnifyjp/omnify-win32-x64": "3.23.0"
|
|
44
44
|
}
|
|
45
45
|
}
|
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Generates base + editable service classes
|
|
3
|
-
* or `options.service`.
|
|
2
|
+
* Generates base + editable service classes.
|
|
4
3
|
*
|
|
5
4
|
* Issue #57: Full BaseService codegen with translatable, softDelete, audit,
|
|
6
5
|
* DB::transaction, eagerLoad/eagerCount, lookupFields, defaultSort.
|
|
6
|
+
*
|
|
7
|
+
* Issue #81 (v3.23.0): convention-over-configuration flip. Services are now
|
|
8
|
+
* generated by default for every project `kind: object` schema. Opt out with
|
|
9
|
+
* `options.service: false` (pivots, translation sidecars, audit logs).
|
|
10
|
+
* Property-level flags (`searchable`, `filterable`, `sortable`, `lookupable`,
|
|
11
|
+
* `defaultSort: asc|desc`) are the single source of truth — the legacy
|
|
12
|
+
* `options.service.{searchable,filterable,defaultSort,lookupFields,eagerLoad,
|
|
13
|
+
* eagerCount}` keys are deprecated and emit warnings at generate time.
|
|
7
14
|
*/
|
|
8
15
|
import { SchemaReader } from './schema-reader.js';
|
|
9
16
|
import type { GeneratedFile, PhpConfig } from './types.js';
|
|
10
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Generate service classes for every project object schema unless explicitly
|
|
19
|
+
* opted out via `options.service: false`. Also generates for schemas that
|
|
20
|
+
* still use the legacy `options.api` block (unchanged from v3.22.x).
|
|
21
|
+
*
|
|
22
|
+
* Skipped automatically:
|
|
23
|
+
* - kind != object (pivot, partial, enum, extend)
|
|
24
|
+
* - options.service === false (explicit opt-out)
|
|
25
|
+
* - schemas whose name matches a translation sidecar pattern
|
|
26
|
+
* (`*Translation` auto-generated by the translation-model-generator)
|
|
27
|
+
* - package-owned schemas (those get services in their owning package)
|
|
28
|
+
*/
|
|
11
29
|
export declare function generateServices(reader: SchemaReader, config: PhpConfig): GeneratedFile[];
|
|
@@ -1,28 +1,108 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Generates base + editable service classes
|
|
3
|
-
* or `options.service`.
|
|
2
|
+
* Generates base + editable service classes.
|
|
4
3
|
*
|
|
5
4
|
* Issue #57: Full BaseService codegen with translatable, softDelete, audit,
|
|
6
5
|
* DB::transaction, eagerLoad/eagerCount, lookupFields, defaultSort.
|
|
6
|
+
*
|
|
7
|
+
* Issue #81 (v3.23.0): convention-over-configuration flip. Services are now
|
|
8
|
+
* generated by default for every project `kind: object` schema. Opt out with
|
|
9
|
+
* `options.service: false` (pivots, translation sidecars, audit logs).
|
|
10
|
+
* Property-level flags (`searchable`, `filterable`, `sortable`, `lookupable`,
|
|
11
|
+
* `defaultSort: asc|desc`) are the single source of truth — the legacy
|
|
12
|
+
* `options.service.{searchable,filterable,defaultSort,lookupFields,eagerLoad,
|
|
13
|
+
* eagerCount}` keys are deprecated and emit warnings at generate time.
|
|
7
14
|
*/
|
|
8
15
|
import { toPascalCase, toSnakeCase } from './naming-helper.js';
|
|
9
16
|
import { baseFile, userFile, resolveModularBasePath, resolveModularBaseNamespace } from './types.js';
|
|
10
17
|
// ============================================================================
|
|
11
18
|
// Public entry point
|
|
12
19
|
// ============================================================================
|
|
13
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Generate service classes for every project object schema unless explicitly
|
|
22
|
+
* opted out via `options.service: false`. Also generates for schemas that
|
|
23
|
+
* still use the legacy `options.api` block (unchanged from v3.22.x).
|
|
24
|
+
*
|
|
25
|
+
* Skipped automatically:
|
|
26
|
+
* - kind != object (pivot, partial, enum, extend)
|
|
27
|
+
* - options.service === false (explicit opt-out)
|
|
28
|
+
* - schemas whose name matches a translation sidecar pattern
|
|
29
|
+
* (`*Translation` auto-generated by the translation-model-generator)
|
|
30
|
+
* - package-owned schemas (those get services in their owning package)
|
|
31
|
+
*/
|
|
14
32
|
export function generateServices(reader, config) {
|
|
15
33
|
const files = [];
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
34
|
+
const candidates = {};
|
|
35
|
+
// Project-owned object schemas — the new default-enabled set (#81).
|
|
36
|
+
for (const [name, schema] of Object.entries(reader.getProjectObjectSchemas())) {
|
|
37
|
+
// #81 explicit opt-out: `service: false` on the schema
|
|
38
|
+
if (schema.options?.service === false) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
// Skip Astrotomic translation sidecars — they're auto-emitted as PHP
|
|
42
|
+
// translation models, not as full services. Heuristic by name suffix.
|
|
43
|
+
if (name.endsWith('Translation')) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
// Skip hidden schemas (options.hidden = true) — intentional internal types
|
|
47
|
+
if (schema.options?.hidden) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
candidates[name] = schema;
|
|
51
|
+
}
|
|
52
|
+
// Legacy `options.api` schemas — these are already in project set above
|
|
53
|
+
// (getSchemasWithApi is a subset of project schemas). We don't need a
|
|
54
|
+
// second pass for them unless someone sets `service: false` + `api: {}`
|
|
55
|
+
// on the same schema, in which case the explicit api opt-in wins.
|
|
56
|
+
for (const [name, schema] of Object.entries(reader.getSchemasWithApi())) {
|
|
57
|
+
if (!candidates[name]) {
|
|
58
|
+
candidates[name] = schema;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Emit deprecation warnings once per schema for legacy service-block keys
|
|
62
|
+
// that now duplicate property-level metadata.
|
|
63
|
+
for (const [name, schema] of Object.entries(candidates)) {
|
|
64
|
+
warnLegacyServiceKeys(name, schema);
|
|
65
|
+
}
|
|
21
66
|
for (const [name, schema] of Object.entries(candidates)) {
|
|
22
67
|
files.push(...generateForSchema(name, schema, reader, config));
|
|
23
68
|
}
|
|
24
69
|
return files;
|
|
25
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Print a deprecation warning (once per schema) for every legacy
|
|
73
|
+
* `options.service.*` key that duplicates property-level metadata.
|
|
74
|
+
* Each warning points at the property-level equivalent so the migration
|
|
75
|
+
* path is self-documenting.
|
|
76
|
+
*/
|
|
77
|
+
function warnLegacyServiceKeys(name, schema) {
|
|
78
|
+
const svc = schema.options?.service;
|
|
79
|
+
if (!svc || typeof svc !== 'object')
|
|
80
|
+
return;
|
|
81
|
+
const deprecations = [];
|
|
82
|
+
if (svc.searchable && svc.searchable.length > 0) {
|
|
83
|
+
deprecations.push({ key: 'searchable', replacement: "property-level `searchable: true` on each field" });
|
|
84
|
+
}
|
|
85
|
+
if (svc.filterable && svc.filterable.length > 0) {
|
|
86
|
+
deprecations.push({ key: 'filterable', replacement: "property-level `filterable: true` on each field" });
|
|
87
|
+
}
|
|
88
|
+
if (svc.defaultSort !== undefined) {
|
|
89
|
+
deprecations.push({ key: 'defaultSort', replacement: "property-level `defaultSort: asc|desc` on exactly one field" });
|
|
90
|
+
}
|
|
91
|
+
if (svc.lookupFields && svc.lookupFields.length > 0) {
|
|
92
|
+
deprecations.push({ key: 'lookupFields', replacement: "property-level `lookupable: true` on each field" });
|
|
93
|
+
}
|
|
94
|
+
if (svc.eagerLoad && svc.eagerLoad.length > 0) {
|
|
95
|
+
deprecations.push({ key: 'eagerLoad', replacement: "override `applyListEagerLoads()` / `applyFindByIdEagerLoads()` / `applyLookupEagerLoads()` in the editable service" });
|
|
96
|
+
}
|
|
97
|
+
if (svc.eagerCount && svc.eagerCount.length > 0) {
|
|
98
|
+
deprecations.push({ key: 'eagerCount', replacement: "override `applyListEagerLoads()` in the editable service and call `$query->withCount(...)`" });
|
|
99
|
+
}
|
|
100
|
+
for (const d of deprecations) {
|
|
101
|
+
console.warn(`[omnify-ts] ${name}.options.service.${d.key} is deprecated (#81) — ` +
|
|
102
|
+
`use ${d.replacement} instead. The legacy key still works for this ` +
|
|
103
|
+
`release but will be removed in a future major version.`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
26
106
|
function generateForSchema(name, schema, reader, config) {
|
|
27
107
|
return [
|
|
28
108
|
generateBaseService(name, schema, reader, config),
|
|
@@ -40,7 +120,11 @@ function resolveFieldLists(schema, reader, name) {
|
|
|
40
120
|
const properties = schema.properties ?? {};
|
|
41
121
|
const propertyOrder = reader.getPropertyOrder(name);
|
|
42
122
|
const expandedProperties = reader.getExpandedProperties(name);
|
|
43
|
-
|
|
123
|
+
// #81: options.service can now be `false` (explicit opt-out). Narrow to
|
|
124
|
+
// an object here so every downstream read works the same on both shapes;
|
|
125
|
+
// the opt-out is filtered before this function is reached.
|
|
126
|
+
const svcRaw = schema.options?.service;
|
|
127
|
+
const svc = (svcRaw && typeof svcRaw === 'object') ? svcRaw : {};
|
|
44
128
|
const searchableFields = [];
|
|
45
129
|
const filterableFields = [];
|
|
46
130
|
const sortableFields = [];
|
|
@@ -173,6 +257,72 @@ function reconstructRelationTokens(items) {
|
|
|
173
257
|
result.push(current);
|
|
174
258
|
return result;
|
|
175
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* #81: resolve the lookup() projection from property-level `lookupable: true`
|
|
262
|
+
* flags. Returns snake_case column names. Always includes `id` at the head
|
|
263
|
+
* of the list because the caller always wants the primary key. Falls back
|
|
264
|
+
* to ['id', 'name', 'slug'] (filtered to actually-present columns) when no
|
|
265
|
+
* property is marked — matches the v3.22.x default for backward compat.
|
|
266
|
+
*/
|
|
267
|
+
function resolveLookupFields(schema) {
|
|
268
|
+
const properties = schema.properties ?? {};
|
|
269
|
+
const order = schema.propertyOrder ?? Object.keys(properties);
|
|
270
|
+
const explicit = [];
|
|
271
|
+
for (const propName of order) {
|
|
272
|
+
const prop = properties[propName];
|
|
273
|
+
if (!prop)
|
|
274
|
+
continue;
|
|
275
|
+
if (prop.lookupable) {
|
|
276
|
+
explicit.push(toSnakeCase(propName));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (explicit.length > 0) {
|
|
280
|
+
// Always prefix with `id` so the caller can key the returned rows.
|
|
281
|
+
return explicit.includes('id') ? explicit : ['id', ...explicit];
|
|
282
|
+
}
|
|
283
|
+
// Default: id + name + slug if those columns exist on the schema
|
|
284
|
+
const defaults = ['id'];
|
|
285
|
+
if (properties['name'])
|
|
286
|
+
defaults.push('name');
|
|
287
|
+
if (properties['slug'])
|
|
288
|
+
defaults.push('slug');
|
|
289
|
+
return defaults;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* #81: resolve the default sort from a property-level `defaultSort: asc|desc`
|
|
293
|
+
* flag. At most one property per schema should carry this; if multiple are
|
|
294
|
+
* present, the first one wins and a warning is printed. Falls back to the
|
|
295
|
+
* legacy `svc.defaultSort` string (deprecation warning fired elsewhere)
|
|
296
|
+
* then to `-created_at`.
|
|
297
|
+
*/
|
|
298
|
+
function resolveDefaultSort(schema, svc) {
|
|
299
|
+
const properties = schema.properties ?? {};
|
|
300
|
+
const order = schema.propertyOrder ?? Object.keys(properties);
|
|
301
|
+
let picked = null;
|
|
302
|
+
const collisions = [];
|
|
303
|
+
for (const propName of order) {
|
|
304
|
+
const prop = properties[propName];
|
|
305
|
+
if (!prop)
|
|
306
|
+
continue;
|
|
307
|
+
const dir = prop.defaultSort;
|
|
308
|
+
if (dir !== 'asc' && dir !== 'desc')
|
|
309
|
+
continue;
|
|
310
|
+
if (picked) {
|
|
311
|
+
collisions.push(propName);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
picked = { colName: toSnakeCase(propName), direction: dir };
|
|
315
|
+
}
|
|
316
|
+
if (collisions.length > 0 && picked) {
|
|
317
|
+
console.warn(`[omnify-ts] ${schema.name}: multiple properties declare defaultSort ` +
|
|
318
|
+
`(${collisions.join(', ')}); using '${picked.colName}' and ignoring ` +
|
|
319
|
+
`the rest. Remove the extras to silence this warning.`);
|
|
320
|
+
}
|
|
321
|
+
if (picked) {
|
|
322
|
+
return picked.direction === 'desc' ? `-${picked.colName}` : picked.colName;
|
|
323
|
+
}
|
|
324
|
+
return svc.defaultSort ?? '-created_at';
|
|
325
|
+
}
|
|
176
326
|
/** Collect properties that have non-null defaults (for create() method). */
|
|
177
327
|
function resolveDefaults(schema, reader, name) {
|
|
178
328
|
const properties = schema.properties ?? {};
|
|
@@ -206,11 +356,23 @@ function generateBaseService(name, schema, reader, config) {
|
|
|
206
356
|
const modelName = toPascalCase(name);
|
|
207
357
|
const options = schema.options ?? {};
|
|
208
358
|
const api = options.api ?? {};
|
|
209
|
-
|
|
359
|
+
// #81: options.service can be `false` for explicit opt-out — narrow to
|
|
360
|
+
// a plain object so downstream reads of svc.* are always safe. The
|
|
361
|
+
// opt-out filter in generateServices() means we never reach this path
|
|
362
|
+
// for schemas that set `service: false`.
|
|
363
|
+
const svcRaw = options.service;
|
|
364
|
+
const svc = (svcRaw && typeof svcRaw === 'object') ? svcRaw : {};
|
|
210
365
|
const hasSoftDelete = options.softDelete ?? false;
|
|
211
366
|
const perPage = api.perPage ?? 25;
|
|
212
|
-
|
|
213
|
-
|
|
367
|
+
// #81: lookup fields come from property-level `lookupable: true`.
|
|
368
|
+
// Legacy `options.service.lookupFields` still honoured (deprecation warning
|
|
369
|
+
// fired earlier). Always emit lookup() — it's a free method on every
|
|
370
|
+
// generated service under the opt-out default.
|
|
371
|
+
const lookupFieldsFromProps = resolveLookupFields(schema);
|
|
372
|
+
const lookupFields = (svc.lookupFields && svc.lookupFields.length > 0)
|
|
373
|
+
? [...svc.lookupFields]
|
|
374
|
+
: lookupFieldsFromProps;
|
|
375
|
+
const lookup = lookupFields.length > 0;
|
|
214
376
|
// Audit detection — schema-level then global
|
|
215
377
|
const schemaAudit = options.audit;
|
|
216
378
|
const globalAudit = reader.getAuditConfig();
|
|
@@ -228,13 +390,14 @@ function generateBaseService(name, schema, reader, config) {
|
|
|
228
390
|
fkColumn: `${modelSnake}_id`,
|
|
229
391
|
translatableFields,
|
|
230
392
|
};
|
|
231
|
-
// Eager load / count
|
|
393
|
+
// Eager load / count — legacy YAML-declared (deprecated in #81, still
|
|
394
|
+
// honoured during the deprecation cycle). New code should override
|
|
395
|
+
// applyListEagerLoads() / applyFindByIdEagerLoads() / applyLookupEagerLoads()
|
|
396
|
+
// in the editable service.
|
|
232
397
|
const eagerLoad = reconstructRelationTokens(svc.eagerLoad ?? []);
|
|
233
398
|
const eagerCount = svc.eagerCount ?? [];
|
|
234
|
-
// Default sort
|
|
235
|
-
const defaultSort = svc
|
|
236
|
-
// Lookup fields
|
|
237
|
-
const lookupFields = svc.lookupFields ?? ['id', 'name'];
|
|
399
|
+
// Default sort — #81: property-level `defaultSort: asc|desc` takes priority.
|
|
400
|
+
const defaultSort = resolveDefaultSort(schema, svc);
|
|
238
401
|
// Field lists
|
|
239
402
|
const { searchableFields, filterableFields, sortableFields } = resolveFieldLists(schema, reader, name);
|
|
240
403
|
// Property defaults
|
|
@@ -293,6 +456,10 @@ function generateBaseService(name, schema, reader, config) {
|
|
|
293
456
|
sections.push(buildSectionComment('Lookup'));
|
|
294
457
|
sections.push(buildLookupMethod(modelName, lookupFields, filterableFields, defaultSort, translationCtx, reader, hasSoftDelete));
|
|
295
458
|
}
|
|
459
|
+
// ── Eager Load Hooks (#81) ────────────────────────────────────────────────
|
|
460
|
+
sections.push('');
|
|
461
|
+
sections.push(buildSectionComment('Eager Load Hooks (#81)'));
|
|
462
|
+
sections.push(buildEagerLoadHooks());
|
|
296
463
|
// ── Translatable helpers ──────────────────────────────────────────────────
|
|
297
464
|
if (hasTranslatable) {
|
|
298
465
|
sections.push('');
|
|
@@ -340,6 +507,63 @@ function buildSectionComment(title) {
|
|
|
340
507
|
// ${title}
|
|
341
508
|
// =========================================================================`;
|
|
342
509
|
}
|
|
510
|
+
/**
|
|
511
|
+
* #81: empty protected hook methods that every read path calls after
|
|
512
|
+
* building its query. Consumers override these in the editable sibling
|
|
513
|
+
* service to apply per-method eager loads, keeping eager loading as a
|
|
514
|
+
* query-time concern (as every other ORM in the ecosystem does) instead
|
|
515
|
+
* of a schema-level one.
|
|
516
|
+
*
|
|
517
|
+
* The legacy `options.service.eagerLoad` / `eagerCount` YAML config is
|
|
518
|
+
* still honoured in v3.23.x — its emitted `->with(...)->withCount(...)`
|
|
519
|
+
* chain runs BEFORE the hook, so overrides compose additively. The YAML
|
|
520
|
+
* config will be removed in a future major release per the #81
|
|
521
|
+
* deprecation path.
|
|
522
|
+
*/
|
|
523
|
+
function buildEagerLoadHooks() {
|
|
524
|
+
return `
|
|
525
|
+
/**
|
|
526
|
+
* Override in the editable sibling service to eager-load relations
|
|
527
|
+
* on the list() query. Default: no-op. Legacy schema-level eagerLoad
|
|
528
|
+
* configuration still applies before this hook runs.
|
|
529
|
+
*
|
|
530
|
+
* @param \\Illuminate\\Database\\Eloquent\\Builder $query
|
|
531
|
+
*/
|
|
532
|
+
protected function applyListEagerLoads($query): void
|
|
533
|
+
{
|
|
534
|
+
// Override in your editable service:
|
|
535
|
+
// $query->with(['author', 'category'])->withCount(['comments']);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Override in the editable sibling service to eager-load relations on
|
|
540
|
+
* the findById() query. Default: no-op.
|
|
541
|
+
*
|
|
542
|
+
* @param \\Illuminate\\Database\\Eloquent\\Builder $query
|
|
543
|
+
*/
|
|
544
|
+
protected function applyFindByIdEagerLoads($query): void
|
|
545
|
+
{
|
|
546
|
+
// Override in your editable service for detail-page eager loads:
|
|
547
|
+
// $query->with([
|
|
548
|
+
// 'author:id,name',
|
|
549
|
+
// 'category',
|
|
550
|
+
// 'comments.author',
|
|
551
|
+
// ]);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Override in the editable sibling service to eager-load relations on
|
|
556
|
+
* the lookup() query. Default: no-op. Most lookup endpoints should
|
|
557
|
+
* stay lean — eager loading defeats the point of a dropdown projection.
|
|
558
|
+
*
|
|
559
|
+
* @param \\Illuminate\\Database\\Eloquent\\Builder $query
|
|
560
|
+
*/
|
|
561
|
+
protected function applyLookupEagerLoads($query): void
|
|
562
|
+
{
|
|
563
|
+
// Override only when the lookup result needs related data that
|
|
564
|
+
// can't come from the sidecar translation table.
|
|
565
|
+
}`;
|
|
566
|
+
}
|
|
343
567
|
// ============================================================================
|
|
344
568
|
// #77 follow-up: strict @param array-shape + @example block derivation
|
|
345
569
|
// ============================================================================
|
|
@@ -733,7 +957,7 @@ ${listExample}
|
|
|
733
957
|
public function list(array $filters = []): LengthAwarePaginator
|
|
734
958
|
{
|
|
735
959
|
$query = $this->model::query()`);
|
|
736
|
-
// Eager load
|
|
960
|
+
// Eager load (legacy YAML-configured, deprecated #81)
|
|
737
961
|
const eagerChain = buildEagerLoadChain(eagerLoad, eagerCount);
|
|
738
962
|
if (eagerChain) {
|
|
739
963
|
lines.push(`${eagerChain};`);
|
|
@@ -742,6 +966,11 @@ ${listExample}
|
|
|
742
966
|
// Close the query chain
|
|
743
967
|
lines[lines.length - 1] += ';';
|
|
744
968
|
}
|
|
969
|
+
// #81: user-overridable eager load hook. Runs after the legacy chain so
|
|
970
|
+
// subclass overrides compose additively on top of whatever YAML declared.
|
|
971
|
+
lines.push('');
|
|
972
|
+
lines.push(' // -- Eager loads via #81 hook (override applyListEagerLoads in sibling service) --');
|
|
973
|
+
lines.push(' $this->applyListEagerLoads($query);');
|
|
745
974
|
// Filterable
|
|
746
975
|
if (filterableFields.length > 0) {
|
|
747
976
|
lines.push('');
|
|
@@ -888,7 +1117,9 @@ function buildSortSection(_sortableFields, defaultSort, allowedSortColumns, tx)
|
|
|
888
1117
|
// ── findById() ──────────────────────────────────────────────────────────────
|
|
889
1118
|
function buildFindByIdMethod(modelName, eagerLoad, eagerCount, hasSoftDelete) {
|
|
890
1119
|
const eagerChain = buildEagerLoadChain(eagerLoad, eagerCount);
|
|
891
|
-
|
|
1120
|
+
// Preserve a leading newline + the chain, but NO trailing whitespace so
|
|
1121
|
+
// the semicolon lands cleanly in `$query = ...${chain};`
|
|
1122
|
+
const chain = eagerChain ? `\n${eagerChain}` : '';
|
|
892
1123
|
if (!hasSoftDelete) {
|
|
893
1124
|
return `
|
|
894
1125
|
/**
|
|
@@ -902,7 +1133,10 @@ function buildFindByIdMethod(modelName, eagerLoad, eagerCount, hasSoftDelete) {
|
|
|
902
1133
|
*/
|
|
903
1134
|
public function findById(string $id): ${modelName}
|
|
904
1135
|
{
|
|
905
|
-
|
|
1136
|
+
$query = $this->model::query()${chain};
|
|
1137
|
+
$this->applyFindByIdEagerLoads($query);
|
|
1138
|
+
|
|
1139
|
+
return $query->findOrFail($id);
|
|
906
1140
|
}`;
|
|
907
1141
|
}
|
|
908
1142
|
// #79: soft-deletable schemas accept an opt-in `$withTrashed` flag so admin
|
|
@@ -1248,6 +1482,10 @@ ${lookupExample}
|
|
|
1248
1482
|
->select([${selectFields}])
|
|
1249
1483
|
->orderBy('${sortCol}', '${sortDir}');
|
|
1250
1484
|
|
|
1485
|
+
// #81: override applyLookupEagerLoads in sibling service to add any
|
|
1486
|
+
// relations that the dropdown projection needs (keep it lean!).
|
|
1487
|
+
$this->applyLookupEagerLoads($query);
|
|
1488
|
+
|
|
1251
1489
|
${filterBlock}${softDeleteBlock}${limitBlock}
|
|
1252
1490
|
|
|
1253
1491
|
// #76 followup (v3.21.2): use getAttributes() to bypass Astrotomic's
|
|
@@ -27,7 +27,12 @@ export function generateTsServices(schemas, options) {
|
|
|
27
27
|
// Per-schema generator
|
|
28
28
|
// ---------------------------------------------------------------------------
|
|
29
29
|
function generateServiceFile(name, schema, options) {
|
|
30
|
-
|
|
30
|
+
// #81: options.service can be `false` for explicit opt-out. Treat `false`
|
|
31
|
+
// the same as an empty service block for TS codegen purposes — the Go
|
|
32
|
+
// side already filtered this path when emitting the PHP base, but the
|
|
33
|
+
// TS side runs independently and needs its own narrowing.
|
|
34
|
+
const svcRaw = schema.options?.service;
|
|
35
|
+
const svc = (svcRaw && typeof svcRaw === 'object') ? svcRaw : {};
|
|
31
36
|
const api = schema.options?.api;
|
|
32
37
|
const hasSoftDelete = schema.options?.softDelete ?? false;
|
|
33
38
|
const hasRestore = api?.restore ?? hasSoftDelete;
|
package/ts-dist/types.d.ts
CHANGED
|
@@ -117,13 +117,32 @@ export interface ApiOptions {
|
|
|
117
117
|
readonly middleware?: readonly string[];
|
|
118
118
|
readonly perPage?: number;
|
|
119
119
|
}
|
|
120
|
-
/**
|
|
120
|
+
/**
|
|
121
|
+
* Service layer codegen options — controls BaseService generation. Issue #57.
|
|
122
|
+
*
|
|
123
|
+
* #81 (v3.23.0): every field in this interface is DEPRECATED. Use the
|
|
124
|
+
* property-level flags instead (searchable / filterable / sortable /
|
|
125
|
+
* lookupable / defaultSort), which are the single source of truth. This
|
|
126
|
+
* interface is kept for backward compatibility and emits deprecation
|
|
127
|
+
* warnings from the generator when any legacy key is set.
|
|
128
|
+
*
|
|
129
|
+
* The only keys that will survive the deprecation cycle are legitimate
|
|
130
|
+
* service-level overrides that property-level can't express (pagination
|
|
131
|
+
* strategy, per_page default, etc.) — those will be added in a future
|
|
132
|
+
* release when the deprecations clear.
|
|
133
|
+
*/
|
|
121
134
|
export interface ServiceOptions {
|
|
135
|
+
/** @deprecated #81 — use property-level `searchable: true` instead */
|
|
122
136
|
readonly searchable?: readonly string[];
|
|
137
|
+
/** @deprecated #81 — use property-level `filterable: true` instead */
|
|
123
138
|
readonly filterable?: readonly string[];
|
|
139
|
+
/** @deprecated #81 — use property-level `defaultSort: 'asc'|'desc'` on exactly one field */
|
|
124
140
|
readonly defaultSort?: string;
|
|
141
|
+
/** @deprecated #81 — override applyListEagerLoads/applyFindByIdEagerLoads/applyLookupEagerLoads in the editable service instead */
|
|
125
142
|
readonly eagerLoad?: readonly string[];
|
|
143
|
+
/** @deprecated #81 — override applyListEagerLoads hook in the editable service instead */
|
|
126
144
|
readonly eagerCount?: readonly string[];
|
|
145
|
+
/** @deprecated #81 — use property-level `lookupable: true` instead */
|
|
127
146
|
readonly lookupFields?: readonly string[];
|
|
128
147
|
}
|
|
129
148
|
/** Schema options. */
|
|
@@ -142,8 +161,18 @@ export interface SchemaOptions {
|
|
|
142
161
|
readonly updatedBy?: boolean;
|
|
143
162
|
readonly deletedBy?: boolean;
|
|
144
163
|
};
|
|
145
|
-
/**
|
|
146
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Service layer codegen options (issue #57).
|
|
166
|
+
*
|
|
167
|
+
* #81 (v3.23.0): accepts `false` as an explicit opt-out — every `kind:
|
|
168
|
+
* object` project schema gets a generated base service by default now.
|
|
169
|
+
* Set `service: false` on pivot tables / sidecars / audit logs that
|
|
170
|
+
* shouldn't have a service. Set to an object only for legitimate
|
|
171
|
+
* service-level overrides that property-level flags can't express
|
|
172
|
+
* (legacy property-duplication keys emit deprecation warnings at
|
|
173
|
+
* generate time).
|
|
174
|
+
*/
|
|
175
|
+
readonly service?: ServiceOptions | false;
|
|
147
176
|
/** Schema-level default ordering — generates a global Eloquent scope. Issue #40. */
|
|
148
177
|
readonly defaultOrder?: readonly OrderByItem[];
|
|
149
178
|
}
|
|
@@ -264,6 +293,21 @@ export interface PropertyDefinition {
|
|
|
264
293
|
readonly searchable?: boolean;
|
|
265
294
|
readonly filterable?: boolean;
|
|
266
295
|
readonly sortable?: boolean;
|
|
296
|
+
/**
|
|
297
|
+
* #81: mark a property as part of the lookup() projection (id/label/slug
|
|
298
|
+
* shape for type-ahead / dropdown endpoints). When at least one property
|
|
299
|
+
* on a schema has `lookupable: true`, the generator uses that set instead
|
|
300
|
+
* of the legacy `options.service.lookupFields` array.
|
|
301
|
+
*/
|
|
302
|
+
readonly lookupable?: boolean;
|
|
303
|
+
/**
|
|
304
|
+
* #81: declare this property as the schema's default sort column. At most
|
|
305
|
+
* one property per schema should set this. The value is the direction:
|
|
306
|
+
* `asc` emits `defaultSort: <col>`, `desc` emits `-<col>`. Legacy
|
|
307
|
+
* `options.service.defaultSort` is still honoured with a deprecation
|
|
308
|
+
* warning.
|
|
309
|
+
*/
|
|
310
|
+
readonly defaultSort?: 'asc' | 'desc';
|
|
267
311
|
readonly fields?: Record<string, FieldOverride>;
|
|
268
312
|
}
|
|
269
313
|
/** Single column ordering. Direction defaults to 'asc'. Issue #40. */
|