@omnifyjp/omnify 3.22.1 → 3.23.1
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.1",
|
|
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.1",
|
|
40
|
+
"@omnifyjp/omnify-darwin-x64": "3.23.1",
|
|
41
|
+
"@omnifyjp/omnify-linux-x64": "3.23.1",
|
|
42
|
+
"@omnifyjp/omnify-linux-arm64": "3.23.1",
|
|
43
|
+
"@omnifyjp/omnify-win32-x64": "3.23.1"
|
|
44
44
|
}
|
|
45
45
|
}
|
package/ts-dist/php/index.js
CHANGED
|
@@ -68,10 +68,12 @@ export function generatePhp(data, overrides) {
|
|
|
68
68
|
// we add the standalone Common / Info / Schemas files.
|
|
69
69
|
files.push(...generateOpenApi(reader, config));
|
|
70
70
|
}
|
|
71
|
-
// Service layer (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
// Service layer. #81 (v3.23.0): service generation is now opt-out by
|
|
72
|
+
// default — every project `kind: object` schema gets a base service
|
|
73
|
+
// unless explicitly opted out via `options.service: false`. The old
|
|
74
|
+
// opt-in gate here is gone because `generateServices` does its own
|
|
75
|
+
// filter internally; always call it and let it decide.
|
|
76
|
+
files.push(...generateServices(reader, config));
|
|
75
77
|
// Issue #34: every `kind: enum` schema becomes a global PHP enum class.
|
|
76
78
|
files.push(...generateEnums(reader, config));
|
|
77
79
|
// Astrotomic translatable config (only when at least one schema has
|
|
@@ -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,167 @@
|
|
|
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
|
+
// #81 + #80 interaction (v3.23.1): iterate every project-owned schema
|
|
36
|
+
// directly instead of going through `getProjectObjectSchemas()`, which
|
|
37
|
+
// filters out KindPartial. Phantom-upstream extends (#80 self-reference
|
|
38
|
+
// fallback — e.g. `kind: extend, target: Product` with no upstream
|
|
39
|
+
// package loaded) are kept as KindPartial so downstream generators
|
|
40
|
+
// skip emitting tables for them, but their SERVICES still belong in
|
|
41
|
+
// the consumer project and must be generated here. Kind=object stays
|
|
42
|
+
// the opt-out default; Kind=partial/extend is included when the user
|
|
43
|
+
// has signalled service intent (legacy options.service block or any
|
|
44
|
+
// property-level service flag).
|
|
45
|
+
const allSchemas = reader.getSchemas();
|
|
46
|
+
for (const [name, schema] of Object.entries(allSchemas)) {
|
|
47
|
+
// Project-owned only — packages emit their own services in the
|
|
48
|
+
// owning package's codegen pass.
|
|
49
|
+
if (schema.package != null)
|
|
50
|
+
continue;
|
|
51
|
+
const kind = schema.kind ?? 'object';
|
|
52
|
+
// Hard exclusions: non-object-ish kinds never get services
|
|
53
|
+
if (kind === 'enum' || kind === 'pivot')
|
|
54
|
+
continue;
|
|
55
|
+
// #81 explicit opt-out: `service: false` on the schema
|
|
56
|
+
if (schema.options?.service === false) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
// Astrotomic translation sidecars — auto-emitted as PHP translation
|
|
60
|
+
// models, not full services. Heuristic by name suffix.
|
|
61
|
+
if (name.endsWith('Translation')) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// Hidden schemas (options.hidden = true) — intentional internal types
|
|
65
|
+
if (schema.options?.hidden) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (kind === 'object') {
|
|
69
|
+
// Phase 2 default: every object schema gets a service
|
|
70
|
+
candidates[name] = schema;
|
|
71
|
+
}
|
|
72
|
+
else if (kind === 'partial' || kind === 'extend') {
|
|
73
|
+
// Phantom-upstream extend (#80 fallback). Only emit a service if the
|
|
74
|
+
// user signalled intent — either a legacy options.service block, or
|
|
75
|
+
// at least one property-level service flag (searchable / filterable /
|
|
76
|
+
// sortable / lookupable / defaultSort). An empty extend with no
|
|
77
|
+
// service config is not a service candidate — it's a stub the user
|
|
78
|
+
// added for property merging elsewhere.
|
|
79
|
+
if (schemaHasServiceIntent(schema)) {
|
|
80
|
+
candidates[name] = schema;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Legacy `options.api` schemas — these are already in project set above
|
|
85
|
+
// for most cases, but belt-and-suspenders: include any that slipped
|
|
86
|
+
// through the kind filter.
|
|
87
|
+
for (const [name, schema] of Object.entries(reader.getSchemasWithApi())) {
|
|
88
|
+
if (!candidates[name]) {
|
|
89
|
+
candidates[name] = schema;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Emit deprecation warnings once per schema for legacy service-block keys
|
|
93
|
+
// that now duplicate property-level metadata.
|
|
94
|
+
for (const [name, schema] of Object.entries(candidates)) {
|
|
95
|
+
warnLegacyServiceKeys(name, schema);
|
|
96
|
+
}
|
|
21
97
|
for (const [name, schema] of Object.entries(candidates)) {
|
|
22
98
|
files.push(...generateForSchema(name, schema, reader, config));
|
|
23
99
|
}
|
|
24
100
|
return files;
|
|
25
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* #81 + #80 (v3.23.1): detect "user wants a service for this schema" signal
|
|
104
|
+
* on extend/partial schemas. Returns true when either:
|
|
105
|
+
* - the legacy options.service block is present (truthy, not false), OR
|
|
106
|
+
* - at least one property has a service-related flag set.
|
|
107
|
+
*
|
|
108
|
+
* Phantom-upstream extends (#80 self-reference fallback) stay as KindPartial
|
|
109
|
+
* for migration purposes, but the consumer project still wants a service
|
|
110
|
+
* layer for calling into the externally-owned table. This helper tells the
|
|
111
|
+
* candidate selector to include those.
|
|
112
|
+
*/
|
|
113
|
+
function schemaHasServiceIntent(schema) {
|
|
114
|
+
const svc = schema.options?.service;
|
|
115
|
+
if (svc && typeof svc === 'object')
|
|
116
|
+
return true;
|
|
117
|
+
const properties = schema.properties ?? {};
|
|
118
|
+
for (const prop of Object.values(properties)) {
|
|
119
|
+
if (!prop)
|
|
120
|
+
continue;
|
|
121
|
+
if (prop.searchable || prop.filterable || prop.sortable)
|
|
122
|
+
return true;
|
|
123
|
+
if (prop.lookupable)
|
|
124
|
+
return true;
|
|
125
|
+
if (prop.defaultSort)
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Print a deprecation warning (once per schema) for every legacy
|
|
132
|
+
* `options.service.*` key that duplicates property-level metadata.
|
|
133
|
+
* Each warning points at the property-level equivalent so the migration
|
|
134
|
+
* path is self-documenting.
|
|
135
|
+
*/
|
|
136
|
+
function warnLegacyServiceKeys(name, schema) {
|
|
137
|
+
const svc = schema.options?.service;
|
|
138
|
+
if (!svc || typeof svc !== 'object')
|
|
139
|
+
return;
|
|
140
|
+
const deprecations = [];
|
|
141
|
+
if (svc.searchable && svc.searchable.length > 0) {
|
|
142
|
+
deprecations.push({ key: 'searchable', replacement: "property-level `searchable: true` on each field" });
|
|
143
|
+
}
|
|
144
|
+
if (svc.filterable && svc.filterable.length > 0) {
|
|
145
|
+
deprecations.push({ key: 'filterable', replacement: "property-level `filterable: true` on each field" });
|
|
146
|
+
}
|
|
147
|
+
if (svc.defaultSort !== undefined) {
|
|
148
|
+
deprecations.push({ key: 'defaultSort', replacement: "property-level `defaultSort: asc|desc` on exactly one field" });
|
|
149
|
+
}
|
|
150
|
+
if (svc.lookupFields && svc.lookupFields.length > 0) {
|
|
151
|
+
deprecations.push({ key: 'lookupFields', replacement: "property-level `lookupable: true` on each field" });
|
|
152
|
+
}
|
|
153
|
+
if (svc.eagerLoad && svc.eagerLoad.length > 0) {
|
|
154
|
+
deprecations.push({ key: 'eagerLoad', replacement: "override `applyListEagerLoads()` / `applyFindByIdEagerLoads()` / `applyLookupEagerLoads()` in the editable service" });
|
|
155
|
+
}
|
|
156
|
+
if (svc.eagerCount && svc.eagerCount.length > 0) {
|
|
157
|
+
deprecations.push({ key: 'eagerCount', replacement: "override `applyListEagerLoads()` in the editable service and call `$query->withCount(...)`" });
|
|
158
|
+
}
|
|
159
|
+
for (const d of deprecations) {
|
|
160
|
+
console.warn(`[omnify-ts] ${name}.options.service.${d.key} is deprecated (#81) — ` +
|
|
161
|
+
`use ${d.replacement} instead. The legacy key still works for this ` +
|
|
162
|
+
`release but will be removed in a future major version.`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
26
165
|
function generateForSchema(name, schema, reader, config) {
|
|
27
166
|
return [
|
|
28
167
|
generateBaseService(name, schema, reader, config),
|
|
@@ -40,7 +179,11 @@ function resolveFieldLists(schema, reader, name) {
|
|
|
40
179
|
const properties = schema.properties ?? {};
|
|
41
180
|
const propertyOrder = reader.getPropertyOrder(name);
|
|
42
181
|
const expandedProperties = reader.getExpandedProperties(name);
|
|
43
|
-
|
|
182
|
+
// #81: options.service can now be `false` (explicit opt-out). Narrow to
|
|
183
|
+
// an object here so every downstream read works the same on both shapes;
|
|
184
|
+
// the opt-out is filtered before this function is reached.
|
|
185
|
+
const svcRaw = schema.options?.service;
|
|
186
|
+
const svc = (svcRaw && typeof svcRaw === 'object') ? svcRaw : {};
|
|
44
187
|
const searchableFields = [];
|
|
45
188
|
const filterableFields = [];
|
|
46
189
|
const sortableFields = [];
|
|
@@ -173,6 +316,72 @@ function reconstructRelationTokens(items) {
|
|
|
173
316
|
result.push(current);
|
|
174
317
|
return result;
|
|
175
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* #81: resolve the lookup() projection from property-level `lookupable: true`
|
|
321
|
+
* flags. Returns snake_case column names. Always includes `id` at the head
|
|
322
|
+
* of the list because the caller always wants the primary key. Falls back
|
|
323
|
+
* to ['id', 'name', 'slug'] (filtered to actually-present columns) when no
|
|
324
|
+
* property is marked — matches the v3.22.x default for backward compat.
|
|
325
|
+
*/
|
|
326
|
+
function resolveLookupFields(schema) {
|
|
327
|
+
const properties = schema.properties ?? {};
|
|
328
|
+
const order = schema.propertyOrder ?? Object.keys(properties);
|
|
329
|
+
const explicit = [];
|
|
330
|
+
for (const propName of order) {
|
|
331
|
+
const prop = properties[propName];
|
|
332
|
+
if (!prop)
|
|
333
|
+
continue;
|
|
334
|
+
if (prop.lookupable) {
|
|
335
|
+
explicit.push(toSnakeCase(propName));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (explicit.length > 0) {
|
|
339
|
+
// Always prefix with `id` so the caller can key the returned rows.
|
|
340
|
+
return explicit.includes('id') ? explicit : ['id', ...explicit];
|
|
341
|
+
}
|
|
342
|
+
// Default: id + name + slug if those columns exist on the schema
|
|
343
|
+
const defaults = ['id'];
|
|
344
|
+
if (properties['name'])
|
|
345
|
+
defaults.push('name');
|
|
346
|
+
if (properties['slug'])
|
|
347
|
+
defaults.push('slug');
|
|
348
|
+
return defaults;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* #81: resolve the default sort from a property-level `defaultSort: asc|desc`
|
|
352
|
+
* flag. At most one property per schema should carry this; if multiple are
|
|
353
|
+
* present, the first one wins and a warning is printed. Falls back to the
|
|
354
|
+
* legacy `svc.defaultSort` string (deprecation warning fired elsewhere)
|
|
355
|
+
* then to `-created_at`.
|
|
356
|
+
*/
|
|
357
|
+
function resolveDefaultSort(schema, svc) {
|
|
358
|
+
const properties = schema.properties ?? {};
|
|
359
|
+
const order = schema.propertyOrder ?? Object.keys(properties);
|
|
360
|
+
let picked = null;
|
|
361
|
+
const collisions = [];
|
|
362
|
+
for (const propName of order) {
|
|
363
|
+
const prop = properties[propName];
|
|
364
|
+
if (!prop)
|
|
365
|
+
continue;
|
|
366
|
+
const dir = prop.defaultSort;
|
|
367
|
+
if (dir !== 'asc' && dir !== 'desc')
|
|
368
|
+
continue;
|
|
369
|
+
if (picked) {
|
|
370
|
+
collisions.push(propName);
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
picked = { colName: toSnakeCase(propName), direction: dir };
|
|
374
|
+
}
|
|
375
|
+
if (collisions.length > 0 && picked) {
|
|
376
|
+
console.warn(`[omnify-ts] ${schema.name}: multiple properties declare defaultSort ` +
|
|
377
|
+
`(${collisions.join(', ')}); using '${picked.colName}' and ignoring ` +
|
|
378
|
+
`the rest. Remove the extras to silence this warning.`);
|
|
379
|
+
}
|
|
380
|
+
if (picked) {
|
|
381
|
+
return picked.direction === 'desc' ? `-${picked.colName}` : picked.colName;
|
|
382
|
+
}
|
|
383
|
+
return svc.defaultSort ?? '-created_at';
|
|
384
|
+
}
|
|
176
385
|
/** Collect properties that have non-null defaults (for create() method). */
|
|
177
386
|
function resolveDefaults(schema, reader, name) {
|
|
178
387
|
const properties = schema.properties ?? {};
|
|
@@ -206,11 +415,23 @@ function generateBaseService(name, schema, reader, config) {
|
|
|
206
415
|
const modelName = toPascalCase(name);
|
|
207
416
|
const options = schema.options ?? {};
|
|
208
417
|
const api = options.api ?? {};
|
|
209
|
-
|
|
418
|
+
// #81: options.service can be `false` for explicit opt-out — narrow to
|
|
419
|
+
// a plain object so downstream reads of svc.* are always safe. The
|
|
420
|
+
// opt-out filter in generateServices() means we never reach this path
|
|
421
|
+
// for schemas that set `service: false`.
|
|
422
|
+
const svcRaw = options.service;
|
|
423
|
+
const svc = (svcRaw && typeof svcRaw === 'object') ? svcRaw : {};
|
|
210
424
|
const hasSoftDelete = options.softDelete ?? false;
|
|
211
425
|
const perPage = api.perPage ?? 25;
|
|
212
|
-
|
|
213
|
-
|
|
426
|
+
// #81: lookup fields come from property-level `lookupable: true`.
|
|
427
|
+
// Legacy `options.service.lookupFields` still honoured (deprecation warning
|
|
428
|
+
// fired earlier). Always emit lookup() — it's a free method on every
|
|
429
|
+
// generated service under the opt-out default.
|
|
430
|
+
const lookupFieldsFromProps = resolveLookupFields(schema);
|
|
431
|
+
const lookupFields = (svc.lookupFields && svc.lookupFields.length > 0)
|
|
432
|
+
? [...svc.lookupFields]
|
|
433
|
+
: lookupFieldsFromProps;
|
|
434
|
+
const lookup = lookupFields.length > 0;
|
|
214
435
|
// Audit detection — schema-level then global
|
|
215
436
|
const schemaAudit = options.audit;
|
|
216
437
|
const globalAudit = reader.getAuditConfig();
|
|
@@ -228,13 +449,14 @@ function generateBaseService(name, schema, reader, config) {
|
|
|
228
449
|
fkColumn: `${modelSnake}_id`,
|
|
229
450
|
translatableFields,
|
|
230
451
|
};
|
|
231
|
-
// Eager load / count
|
|
452
|
+
// Eager load / count — legacy YAML-declared (deprecated in #81, still
|
|
453
|
+
// honoured during the deprecation cycle). New code should override
|
|
454
|
+
// applyListEagerLoads() / applyFindByIdEagerLoads() / applyLookupEagerLoads()
|
|
455
|
+
// in the editable service.
|
|
232
456
|
const eagerLoad = reconstructRelationTokens(svc.eagerLoad ?? []);
|
|
233
457
|
const eagerCount = svc.eagerCount ?? [];
|
|
234
|
-
// Default sort
|
|
235
|
-
const defaultSort = svc
|
|
236
|
-
// Lookup fields
|
|
237
|
-
const lookupFields = svc.lookupFields ?? ['id', 'name'];
|
|
458
|
+
// Default sort — #81: property-level `defaultSort: asc|desc` takes priority.
|
|
459
|
+
const defaultSort = resolveDefaultSort(schema, svc);
|
|
238
460
|
// Field lists
|
|
239
461
|
const { searchableFields, filterableFields, sortableFields } = resolveFieldLists(schema, reader, name);
|
|
240
462
|
// Property defaults
|
|
@@ -293,6 +515,10 @@ function generateBaseService(name, schema, reader, config) {
|
|
|
293
515
|
sections.push(buildSectionComment('Lookup'));
|
|
294
516
|
sections.push(buildLookupMethod(modelName, lookupFields, filterableFields, defaultSort, translationCtx, reader, hasSoftDelete));
|
|
295
517
|
}
|
|
518
|
+
// ── Eager Load Hooks (#81) ────────────────────────────────────────────────
|
|
519
|
+
sections.push('');
|
|
520
|
+
sections.push(buildSectionComment('Eager Load Hooks (#81)'));
|
|
521
|
+
sections.push(buildEagerLoadHooks());
|
|
296
522
|
// ── Translatable helpers ──────────────────────────────────────────────────
|
|
297
523
|
if (hasTranslatable) {
|
|
298
524
|
sections.push('');
|
|
@@ -340,6 +566,63 @@ function buildSectionComment(title) {
|
|
|
340
566
|
// ${title}
|
|
341
567
|
// =========================================================================`;
|
|
342
568
|
}
|
|
569
|
+
/**
|
|
570
|
+
* #81: empty protected hook methods that every read path calls after
|
|
571
|
+
* building its query. Consumers override these in the editable sibling
|
|
572
|
+
* service to apply per-method eager loads, keeping eager loading as a
|
|
573
|
+
* query-time concern (as every other ORM in the ecosystem does) instead
|
|
574
|
+
* of a schema-level one.
|
|
575
|
+
*
|
|
576
|
+
* The legacy `options.service.eagerLoad` / `eagerCount` YAML config is
|
|
577
|
+
* still honoured in v3.23.x — its emitted `->with(...)->withCount(...)`
|
|
578
|
+
* chain runs BEFORE the hook, so overrides compose additively. The YAML
|
|
579
|
+
* config will be removed in a future major release per the #81
|
|
580
|
+
* deprecation path.
|
|
581
|
+
*/
|
|
582
|
+
function buildEagerLoadHooks() {
|
|
583
|
+
return `
|
|
584
|
+
/**
|
|
585
|
+
* Override in the editable sibling service to eager-load relations
|
|
586
|
+
* on the list() query. Default: no-op. Legacy schema-level eagerLoad
|
|
587
|
+
* configuration still applies before this hook runs.
|
|
588
|
+
*
|
|
589
|
+
* @param \\Illuminate\\Database\\Eloquent\\Builder $query
|
|
590
|
+
*/
|
|
591
|
+
protected function applyListEagerLoads($query): void
|
|
592
|
+
{
|
|
593
|
+
// Override in your editable service:
|
|
594
|
+
// $query->with(['author', 'category'])->withCount(['comments']);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Override in the editable sibling service to eager-load relations on
|
|
599
|
+
* the findById() query. Default: no-op.
|
|
600
|
+
*
|
|
601
|
+
* @param \\Illuminate\\Database\\Eloquent\\Builder $query
|
|
602
|
+
*/
|
|
603
|
+
protected function applyFindByIdEagerLoads($query): void
|
|
604
|
+
{
|
|
605
|
+
// Override in your editable service for detail-page eager loads:
|
|
606
|
+
// $query->with([
|
|
607
|
+
// 'author:id,name',
|
|
608
|
+
// 'category',
|
|
609
|
+
// 'comments.author',
|
|
610
|
+
// ]);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Override in the editable sibling service to eager-load relations on
|
|
615
|
+
* the lookup() query. Default: no-op. Most lookup endpoints should
|
|
616
|
+
* stay lean — eager loading defeats the point of a dropdown projection.
|
|
617
|
+
*
|
|
618
|
+
* @param \\Illuminate\\Database\\Eloquent\\Builder $query
|
|
619
|
+
*/
|
|
620
|
+
protected function applyLookupEagerLoads($query): void
|
|
621
|
+
{
|
|
622
|
+
// Override only when the lookup result needs related data that
|
|
623
|
+
// can't come from the sidecar translation table.
|
|
624
|
+
}`;
|
|
625
|
+
}
|
|
343
626
|
// ============================================================================
|
|
344
627
|
// #77 follow-up: strict @param array-shape + @example block derivation
|
|
345
628
|
// ============================================================================
|
|
@@ -733,7 +1016,7 @@ ${listExample}
|
|
|
733
1016
|
public function list(array $filters = []): LengthAwarePaginator
|
|
734
1017
|
{
|
|
735
1018
|
$query = $this->model::query()`);
|
|
736
|
-
// Eager load
|
|
1019
|
+
// Eager load (legacy YAML-configured, deprecated #81)
|
|
737
1020
|
const eagerChain = buildEagerLoadChain(eagerLoad, eagerCount);
|
|
738
1021
|
if (eagerChain) {
|
|
739
1022
|
lines.push(`${eagerChain};`);
|
|
@@ -742,6 +1025,11 @@ ${listExample}
|
|
|
742
1025
|
// Close the query chain
|
|
743
1026
|
lines[lines.length - 1] += ';';
|
|
744
1027
|
}
|
|
1028
|
+
// #81: user-overridable eager load hook. Runs after the legacy chain so
|
|
1029
|
+
// subclass overrides compose additively on top of whatever YAML declared.
|
|
1030
|
+
lines.push('');
|
|
1031
|
+
lines.push(' // -- Eager loads via #81 hook (override applyListEagerLoads in sibling service) --');
|
|
1032
|
+
lines.push(' $this->applyListEagerLoads($query);');
|
|
745
1033
|
// Filterable
|
|
746
1034
|
if (filterableFields.length > 0) {
|
|
747
1035
|
lines.push('');
|
|
@@ -888,7 +1176,9 @@ function buildSortSection(_sortableFields, defaultSort, allowedSortColumns, tx)
|
|
|
888
1176
|
// ── findById() ──────────────────────────────────────────────────────────────
|
|
889
1177
|
function buildFindByIdMethod(modelName, eagerLoad, eagerCount, hasSoftDelete) {
|
|
890
1178
|
const eagerChain = buildEagerLoadChain(eagerLoad, eagerCount);
|
|
891
|
-
|
|
1179
|
+
// Preserve a leading newline + the chain, but NO trailing whitespace so
|
|
1180
|
+
// the semicolon lands cleanly in `$query = ...${chain};`
|
|
1181
|
+
const chain = eagerChain ? `\n${eagerChain}` : '';
|
|
892
1182
|
if (!hasSoftDelete) {
|
|
893
1183
|
return `
|
|
894
1184
|
/**
|
|
@@ -902,7 +1192,10 @@ function buildFindByIdMethod(modelName, eagerLoad, eagerCount, hasSoftDelete) {
|
|
|
902
1192
|
*/
|
|
903
1193
|
public function findById(string $id): ${modelName}
|
|
904
1194
|
{
|
|
905
|
-
|
|
1195
|
+
$query = $this->model::query()${chain};
|
|
1196
|
+
$this->applyFindByIdEagerLoads($query);
|
|
1197
|
+
|
|
1198
|
+
return $query->findOrFail($id);
|
|
906
1199
|
}`;
|
|
907
1200
|
}
|
|
908
1201
|
// #79: soft-deletable schemas accept an opt-in `$withTrashed` flag so admin
|
|
@@ -1248,6 +1541,10 @@ ${lookupExample}
|
|
|
1248
1541
|
->select([${selectFields}])
|
|
1249
1542
|
->orderBy('${sortCol}', '${sortDir}');
|
|
1250
1543
|
|
|
1544
|
+
// #81: override applyLookupEagerLoads in sibling service to add any
|
|
1545
|
+
// relations that the dropdown projection needs (keep it lean!).
|
|
1546
|
+
$this->applyLookupEagerLoads($query);
|
|
1547
|
+
|
|
1251
1548
|
${filterBlock}${softDeleteBlock}${limitBlock}
|
|
1252
1549
|
|
|
1253
1550
|
// #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. */
|