@pattern-stack/codegen 0.26.0 → 0.27.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/CHANGELOG.md +31 -0
- package/dist/{chunk-LLDJS7PJ.js → chunk-IASPGFFK.js} +4 -4
- package/dist/{chunk-N43D57AP.js → chunk-VCXOPBYY.js} +9 -9
- package/dist/{chunk-6M6LZEP6.js → chunk-VDVEGTSW.js} +4 -4
- package/dist/{chunk-VI2VNA6Y.js → chunk-W4JYZSQK.js} +6 -6
- package/dist/runtime/base-classes/index.js +17 -17
- package/dist/runtime/http/pagination.d.ts +151 -0
- package/dist/runtime/http/pagination.js +98 -0
- package/dist/runtime/http/pagination.js.map +1 -0
- package/dist/runtime/subsystems/auth/auth.module.js +1 -1
- package/dist/runtime/subsystems/auth/index.js +6 -6
- package/dist/runtime/subsystems/cache/cache.module.js +1 -1
- package/dist/runtime/subsystems/cache/index.js +3 -3
- package/dist/runtime/subsystems/events/events.module.js +2 -2
- package/dist/runtime/subsystems/events/index.js +4 -4
- package/dist/runtime/subsystems/index.js +55 -55
- package/dist/runtime/subsystems/integration/index.js +21 -21
- package/dist/runtime/subsystems/integration/integration-cursor-store.drizzle-backend.js +2 -2
- package/dist/runtime/subsystems/integration/integration-run-recorder.drizzle-backend.js +2 -2
- package/dist/runtime/subsystems/integration/integration.module.js +5 -5
- package/dist/runtime/subsystems/observability/index.js +3 -3
- package/dist/src/cli/index.js +206 -29
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/index.js +6 -6
- package/package.json +1 -1
- package/runtime/http/pagination.ts +233 -0
- package/templates/entity/new/clean-lite-ps/controller.ejs.t +27 -6
- package/templates/entity/new/clean-lite-ps/dto/list-query.ejs.t +22 -0
- package/templates/entity/new/clean-lite-ps/index.ejs.t +1 -0
- package/templates/entity/new/clean-lite-ps/prompt-extension.js +14 -0
- package/templates/entity/new/clean-lite-ps/use-cases/list.ejs.t +56 -3
- package/templates/entity/new/prompt.js +17 -0
- /package/dist/{chunk-LLDJS7PJ.js.map → chunk-IASPGFFK.js.map} +0 -0
- /package/dist/{chunk-N43D57AP.js.map → chunk-VCXOPBYY.js.map} +0 -0
- /package/dist/{chunk-6M6LZEP6.js.map → chunk-VDVEGTSW.js.map} +0 -0
- /package/dist/{chunk-VI2VNA6Y.js.map → chunk-W4JYZSQK.js.map} +0 -0
package/dist/src/cli/index.js
CHANGED
|
@@ -46,13 +46,16 @@ import {
|
|
|
46
46
|
writeManifest
|
|
47
47
|
} from "../../chunk-K4BQQ2NN.js";
|
|
48
48
|
import "../../chunk-KVOWSC5S.js";
|
|
49
|
-
import "../../chunk-
|
|
49
|
+
import "../../chunk-VCXOPBYY.js";
|
|
50
50
|
import "../../chunk-PRWIX6UW.js";
|
|
51
|
-
import "../../chunk-
|
|
51
|
+
import "../../chunk-W4JYZSQK.js";
|
|
52
52
|
import "../../chunk-EO2QPOKH.js";
|
|
53
53
|
import "../../chunk-SQDOBLBP.js";
|
|
54
|
+
import "../../chunk-YIVQ7KLS.js";
|
|
54
55
|
import "../../chunk-LG57S2SC.js";
|
|
55
|
-
import "../../chunk-
|
|
56
|
+
import "../../chunk-IASPGFFK.js";
|
|
57
|
+
import "../../chunk-S5G3HO7N.js";
|
|
58
|
+
import "../../chunk-MZ6GV4YF.js";
|
|
56
59
|
import "../../chunk-HNWZFNKP.js";
|
|
57
60
|
import "../../chunk-AHV4GDYM.js";
|
|
58
61
|
import "../../chunk-43SBT72G.js";
|
|
@@ -64,9 +67,6 @@ import {
|
|
|
64
67
|
} from "../../chunk-5TK7MEN4.js";
|
|
65
68
|
import "../../chunk-4KNXX6TI.js";
|
|
66
69
|
import "../../chunk-3CJFPU6Q.js";
|
|
67
|
-
import "../../chunk-YIVQ7KLS.js";
|
|
68
|
-
import "../../chunk-S5G3HO7N.js";
|
|
69
|
-
import "../../chunk-MZ6GV4YF.js";
|
|
70
70
|
import "../../chunk-U64T4YZE.js";
|
|
71
71
|
import "../../chunk-2E224ZSN.js";
|
|
72
72
|
|
|
@@ -3838,7 +3838,6 @@ export class ${cls} implements IIntegrationChangeEmitter {
|
|
|
3838
3838
|
}
|
|
3839
3839
|
|
|
3840
3840
|
// src/cli/shared/sink-emission-generator.ts
|
|
3841
|
-
var USER_ID_FIELD = "userId";
|
|
3842
3841
|
function sinkNames(entityClass) {
|
|
3843
3842
|
return {
|
|
3844
3843
|
sinkClass: `${entityClass}Sink`,
|
|
@@ -3857,13 +3856,12 @@ function assertIntegrated(input) {
|
|
|
3857
3856
|
);
|
|
3858
3857
|
}
|
|
3859
3858
|
}
|
|
3860
|
-
function buildWriteBodyLines(input,
|
|
3861
|
-
const
|
|
3862
|
-
(f) => f.camelName
|
|
3859
|
+
function buildWriteBodyLines(input, _n) {
|
|
3860
|
+
const copyThroughLines = input.copyThroughFields.map(
|
|
3861
|
+
(f) => ` ${f.camelName}: record.${f.camelName},`
|
|
3863
3862
|
);
|
|
3864
|
-
const copyThroughLines = input.copyThroughFields.filter((f) => f.camelName !== USER_ID_FIELD).map((f) => ` ${f.camelName}: record.${f.camelName},`);
|
|
3865
3863
|
const fkLines = input.fkExternalKeys.flatMap((fk) => [
|
|
3866
|
-
` // SEAM (FK external key \u2014 null until you widen ${
|
|
3864
|
+
` // SEAM (FK external key \u2014 null until you widen ${_n.projectionType} to carry \`${fk.writeKey}\`):`,
|
|
3867
3865
|
` // Replace null with record.${fk.writeKey} after widening. Write-safe: repo skips null FKs.`,
|
|
3868
3866
|
` ${fk.writeKey}: null,`
|
|
3869
3867
|
]);
|
|
@@ -3882,9 +3880,6 @@ function buildWriteBodyLines(input, n) {
|
|
|
3882
3880
|
...fkLines
|
|
3883
3881
|
);
|
|
3884
3882
|
}
|
|
3885
|
-
if (hasUserIdField) {
|
|
3886
|
-
lines.push(` userId,`);
|
|
3887
|
-
}
|
|
3888
3883
|
return lines;
|
|
3889
3884
|
}
|
|
3890
3885
|
function buildFindViewLines(input) {
|
|
@@ -4259,7 +4254,9 @@ function resolveEntityModuleImports(input) {
|
|
|
4259
4254
|
repoClass,
|
|
4260
4255
|
moduleClass,
|
|
4261
4256
|
repoImportSpecifier: toImportSpecifier(repoFileAbs, assemblyDirAbs, input.aliases),
|
|
4262
|
-
moduleImportSpecifier: toImportSpecifier(moduleFileAbs, assemblyDirAbs, input.aliases)
|
|
4257
|
+
moduleImportSpecifier: toImportSpecifier(moduleFileAbs, assemblyDirAbs, input.aliases),
|
|
4258
|
+
repoFileAbs,
|
|
4259
|
+
moduleFileAbs
|
|
4263
4260
|
};
|
|
4264
4261
|
}
|
|
4265
4262
|
|
|
@@ -4847,7 +4844,12 @@ function emitAdapters(opts) {
|
|
|
4847
4844
|
backendSrcAbs: opts.backendSrcAbs,
|
|
4848
4845
|
aliases
|
|
4849
4846
|
});
|
|
4850
|
-
const
|
|
4847
|
+
const sinkRepoImportSpecifier = toImportSpecifier(
|
|
4848
|
+
loc.repoFileAbs,
|
|
4849
|
+
sinksDir,
|
|
4850
|
+
aliases
|
|
4851
|
+
);
|
|
4852
|
+
const sinkInput = buildSinkInput(def, surface, slugs[0], sinkRepoImportSpecifier);
|
|
4851
4853
|
const basePath = join8(sinksDir, `${entityName}.sink.generated.ts`);
|
|
4852
4854
|
const baseContent = generateSinkBase({ ...sinkInput, mode });
|
|
4853
4855
|
if (!opts.dryRun) writeIfChanged(basePath, baseContent);
|
|
@@ -5341,6 +5343,56 @@ function buildClientFile(ctx) {
|
|
|
5341
5343
|
` : "";
|
|
5342
5344
|
const body = `${imports}${baseUrlConst}
|
|
5343
5345
|
|
|
5346
|
+
/** Hard upper bound on a single list fetch \u2014 mirrors the backend pageSize clamp. */
|
|
5347
|
+
export const MAX_PAGE_SIZE = 200;
|
|
5348
|
+
|
|
5349
|
+
/**
|
|
5350
|
+
* Pagination envelope returned by every \`GET /<entities>\` (pagination-by-default).
|
|
5351
|
+
* Mirrors the backend \`Page<T>\` runtime contract
|
|
5352
|
+
* (\`@pattern-stack/codegen/runtime/http/pagination\`); duplicated here so the
|
|
5353
|
+
* generated frontend data layer carries no backend import. \`nextCursor\` is
|
|
5354
|
+
* contract-stable (the backend emits it from day one) \u2014 the offset engine
|
|
5355
|
+
* ignores it on request for now.
|
|
5356
|
+
*/
|
|
5357
|
+
export interface Page<T> {
|
|
5358
|
+
items: T[];
|
|
5359
|
+
page: number;
|
|
5360
|
+
pageCount: number;
|
|
5361
|
+
total: number;
|
|
5362
|
+
pageSize: number;
|
|
5363
|
+
nextCursor: string | null;
|
|
5364
|
+
}
|
|
5365
|
+
|
|
5366
|
+
/**
|
|
5367
|
+
* Request query for a list endpoint: page-based pagination + default sort, plus
|
|
5368
|
+
* arbitrary where-filters (passed through to the backend querystring). All keys
|
|
5369
|
+
* optional \u2014 the unfiltered first page is the default.
|
|
5370
|
+
*/
|
|
5371
|
+
export interface ListQuery {
|
|
5372
|
+
page?: number;
|
|
5373
|
+
cursor?: string;
|
|
5374
|
+
pageSize?: number;
|
|
5375
|
+
sort_by?: string;
|
|
5376
|
+
sort_order?: 'asc' | 'desc';
|
|
5377
|
+
[key: string]: string | number | boolean | null | undefined;
|
|
5378
|
+
}
|
|
5379
|
+
|
|
5380
|
+
/**
|
|
5381
|
+
* Serialize a {@link ListQuery} into a querystring (leading \`?\`), skipping
|
|
5382
|
+
* undefined/null values. Returns \`''\` for an empty/absent query so an
|
|
5383
|
+
* unfiltered \`list()\` hits the bare route.
|
|
5384
|
+
*/
|
|
5385
|
+
export function toListQueryString(query?: ListQuery): string {
|
|
5386
|
+
if (!query) return '';
|
|
5387
|
+
const params = new URLSearchParams();
|
|
5388
|
+
for (const [key, value] of Object.entries(query)) {
|
|
5389
|
+
if (value === undefined || value === null) continue;
|
|
5390
|
+
params.set(key, String(value));
|
|
5391
|
+
}
|
|
5392
|
+
const qs = params.toString();
|
|
5393
|
+
return qs ? \`?\${qs}\` : '';
|
|
5394
|
+
}
|
|
5395
|
+
|
|
5344
5396
|
/**
|
|
5345
5397
|
* Base REST transport for \`api\` sync-mode collections and entity api clients.
|
|
5346
5398
|
* Throws on non-2xx; returns parsed JSON, or \`undefined\` for 204 No Content.
|
|
@@ -5375,11 +5427,41 @@ function buildEntityApiFile(entity, ctx) {
|
|
|
5375
5427
|
const { config } = ctx;
|
|
5376
5428
|
const { camelName, plural, className, name } = entity;
|
|
5377
5429
|
const verb = updateVerb(config.architecture);
|
|
5378
|
-
const body = `import { request } from './client';
|
|
5430
|
+
const body = `import { MAX_PAGE_SIZE, type ListQuery, type Page, request, toListQueryString } from './client';
|
|
5379
5431
|
import type { ${className} } from '${config.dbEntitiesImport}/${name}';
|
|
5380
5432
|
|
|
5381
5433
|
export const ${camelName}Api = {
|
|
5382
|
-
|
|
5434
|
+
/**
|
|
5435
|
+
* Fetch one page of ${plural} (pagination-by-default). Threads page/cursor/
|
|
5436
|
+
* pageSize/sort + arbitrary where-filters into the querystring; returns the
|
|
5437
|
+
* \`Page<${className}>\` envelope. Call with no args for the unfiltered first page.
|
|
5438
|
+
*/
|
|
5439
|
+
list: (query?: ListQuery): Promise<Page<${className}>> =>
|
|
5440
|
+
request<Page<${className}>>('GET', \`/${plural}\${toListQueryString(query)}\`),
|
|
5441
|
+
|
|
5442
|
+
/**
|
|
5443
|
+
* Full-fetch escape hatch (LANDMINE 1): every ${className} across all pages as a
|
|
5444
|
+
* flat array. Pages through the envelope until exhausted so off-page FK
|
|
5445
|
+
* resolution (resolvers/lookups) stays correct under pagination-by-default \u2014
|
|
5446
|
+
* the backing collection only holds the current page, so FK targets that live
|
|
5447
|
+
* on another page would otherwise resolve to undefined. Used to hydrate the
|
|
5448
|
+
* store's resolvers/lookups; NOT for rendering a paged table.
|
|
5449
|
+
*/
|
|
5450
|
+
listAll: async (query?: Omit<ListQuery, 'page' | 'pageSize' | 'cursor'>): Promise<${className}[]> => {
|
|
5451
|
+
const all: ${className}[] = [];
|
|
5452
|
+
let page = 1;
|
|
5453
|
+
let pageCount = 1;
|
|
5454
|
+
do {
|
|
5455
|
+
const result = await request<Page<${className}>>(
|
|
5456
|
+
'GET',
|
|
5457
|
+
\`/${plural}\${toListQueryString({ ...query, page, pageSize: MAX_PAGE_SIZE })}\`,
|
|
5458
|
+
);
|
|
5459
|
+
all.push(...result.items);
|
|
5460
|
+
pageCount = result.pageCount;
|
|
5461
|
+
page += 1;
|
|
5462
|
+
} while (page <= pageCount);
|
|
5463
|
+
return all;
|
|
5464
|
+
},
|
|
5383
5465
|
|
|
5384
5466
|
get: (id: string): Promise<${className}> =>
|
|
5385
5467
|
request<${className}>('GET', \`/${plural}/\${id}\`),
|
|
@@ -5511,7 +5593,16 @@ export const ${camelName}Collection = createCollection(
|
|
|
5511
5593
|
id: '${plural}',
|
|
5512
5594
|
queryKey: ['${plural}'],
|
|
5513
5595
|
queryClient,
|
|
5514
|
-
|
|
5596
|
+
// pagination-by-default: the list endpoint returns a Page<T> envelope, so
|
|
5597
|
+
// unwrap \`.items\` to seed the collection with rows (the first page). The
|
|
5598
|
+
// paged table drives later fetches via the sync-layer useList; off-page FK
|
|
5599
|
+
// resolution hydrates from the full-fetch escape hatch (store/resolvers.ts).
|
|
5600
|
+
// async/await (not \`.then\`) so queryCollectionOptions' overload inference
|
|
5601
|
+
// keeps \`getKey\`'s \`item\` typed \u2014 the .then() form collapses it to unknown.
|
|
5602
|
+
queryFn: async () => {
|
|
5603
|
+
const page = await ${camelName}Api.list();
|
|
5604
|
+
return page.items;
|
|
5605
|
+
},
|
|
5515
5606
|
getKey: (item) => item.id,
|
|
5516
5607
|
schema: ${camelName}Schema,
|
|
5517
5608
|
}),
|
|
@@ -5642,21 +5733,33 @@ function buildStoreIndexFile(ctx) {
|
|
|
5642
5733
|
const entities = sortEntities(ctx.entities);
|
|
5643
5734
|
const hookImports = entities.map((e) => `import { ${e.camelName}Hooks } from '../entities/${e.name}';`).join("\n");
|
|
5644
5735
|
const collectionImports = entities.map((e) => `import { ${e.camelName}Collection } from '../collections/${e.name}';`).join("\n");
|
|
5736
|
+
const fieldsImports = entities.map((e) => `import { ${e.camelName}Fields } from '../fields/${e.name}';`).join("\n");
|
|
5645
5737
|
const entityEntries = entities.map((e) => ` ${e.plural}: ${e.camelName}Hooks,`).join("\n");
|
|
5646
5738
|
const collectionEntries = entities.map((e) => ` ${e.plural}: ${e.camelName}Collection,`).join("\n");
|
|
5739
|
+
const fieldsEntries = entities.map((e) => ` ${e.plural}: ${e.camelName}Fields,`).join("\n");
|
|
5647
5740
|
const body = `import { createStore } from '@pattern-stack/frontend-patterns';
|
|
5648
5741
|
|
|
5649
5742
|
${hookImports}
|
|
5650
5743
|
|
|
5651
5744
|
${collectionImports}
|
|
5652
5745
|
|
|
5746
|
+
${fieldsImports}
|
|
5747
|
+
|
|
5748
|
+
import { createLookups } from './lookups';
|
|
5749
|
+
|
|
5653
5750
|
/**
|
|
5654
5751
|
* The application store \u2014 unified access to every entity.
|
|
5655
5752
|
*
|
|
5656
|
-
* Entities and
|
|
5753
|
+
* Entities, collections, and field metadata are keyed by their plural name:
|
|
5754
|
+
* store.${entities[0]?.plural ?? "things"}.useData() // useList + fields[plural] meta + hydrated lookups
|
|
5657
5755
|
* store.${entities[0]?.plural ?? "things"}.useList()
|
|
5658
5756
|
* store.resolve.<entity>(id)
|
|
5659
|
-
* store.lookups.
|
|
5757
|
+
* store.lookups.current
|
|
5758
|
+
*
|
|
5759
|
+
* \`fields\` carries the generated FieldMeta (\`fields/<entity>\` \u2192 \`<camel>Fields\`);
|
|
5760
|
+
* \`lookups\` is the generated lookups engine (\`{ hydrate(): Promise<EntityLookups>;
|
|
5761
|
+
* current }\`) \u2014 hydrated once so off-page FK resolution stays correct under
|
|
5762
|
+
* pagination-by-default. Both bind \`store.<entity>.useData()\`.
|
|
5660
5763
|
*/
|
|
5661
5764
|
export const store = createStore({
|
|
5662
5765
|
entities: {
|
|
@@ -5665,6 +5768,10 @@ ${entityEntries}
|
|
|
5665
5768
|
collections: {
|
|
5666
5769
|
${collectionEntries}
|
|
5667
5770
|
},
|
|
5771
|
+
fields: {
|
|
5772
|
+
${fieldsEntries}
|
|
5773
|
+
},
|
|
5774
|
+
lookups: createLookups(),
|
|
5668
5775
|
});
|
|
5669
5776
|
|
|
5670
5777
|
/** Store type for the \`useStore\` hook. */
|
|
@@ -5675,6 +5782,7 @@ export type AppStore = typeof store;
|
|
|
5675
5782
|
function buildResolversFile(ctx) {
|
|
5676
5783
|
const entities = sortEntities(ctx.entities);
|
|
5677
5784
|
const collectionImports = entities.map((e) => `import { ${e.camelName}Collection } from '../collections/${e.name}';`).join("\n");
|
|
5785
|
+
const apiImports = entities.map((e) => `import { ${e.camelName}Api } from '../api/${e.name}';`).join("\n");
|
|
5678
5786
|
const typeImports = entities.map((e) => `import type { ${e.className} } from '${ctx.config.dbEntitiesImport}/${e.name}';`).join("\n");
|
|
5679
5787
|
const resolverIface = entities.map(
|
|
5680
5788
|
(e) => ` ${e.camelName}: (id: string | null | undefined) => ${e.className} | undefined;`
|
|
@@ -5682,9 +5790,16 @@ function buildResolversFile(ctx) {
|
|
|
5682
5790
|
const resolverImpls = entities.map(
|
|
5683
5791
|
(e) => ` ${e.camelName}: (id) => {
|
|
5684
5792
|
if (!id) return undefined;
|
|
5685
|
-
return ${e.camelName}Collection.state.get(id)
|
|
5793
|
+
return (${e.camelName}Collection.state.get(id) ??
|
|
5794
|
+
hydrationCache.${e.camelName}.get(id)) as ${e.className} | undefined;
|
|
5686
5795
|
},`
|
|
5687
5796
|
).join("\n");
|
|
5797
|
+
const hydrationCacheFields = entities.map((e) => ` ${e.camelName}: new Map<string, ${e.className}>(),`).join("\n");
|
|
5798
|
+
const hydrationCalls = entities.map(
|
|
5799
|
+
(e) => ` ${e.camelName}Api.listAll().then((rows) => {
|
|
5800
|
+
hydrationCache.${e.camelName} = new Map(rows.map((r) => [r.id as string, r]));
|
|
5801
|
+
}),`
|
|
5802
|
+
).join("\n");
|
|
5688
5803
|
const refBlocks = [];
|
|
5689
5804
|
for (const e of entities) {
|
|
5690
5805
|
const rels = resolvableRels(e, ctx);
|
|
@@ -5719,11 +5834,37 @@ ${hydrateFields}
|
|
|
5719
5834
|
${refBlocks.join("\n\n")}
|
|
5720
5835
|
` : "";
|
|
5721
5836
|
const body = `${collectionImports}
|
|
5837
|
+
${apiImports}
|
|
5722
5838
|
${typeImports}
|
|
5723
5839
|
|
|
5840
|
+
/**
|
|
5841
|
+
* Full-fetch hydration cache (LANDMINE 1 escape hatch for pagination-by-default).
|
|
5842
|
+
*
|
|
5843
|
+
* The backing collections hold only the CURRENT PAGE once lists paginate, so an
|
|
5844
|
+
* FK pointing at a row on another page resolves to undefined against collection
|
|
5845
|
+
* state alone. \`hydrateResolverCache()\` fetches the COMPLETE set per entity (via
|
|
5846
|
+
* \`api.listAll()\`, which pages through the envelope) into these id\u2192entity maps;
|
|
5847
|
+
* resolvers fall back to them on a collection-state miss. Call it once on mount.
|
|
5848
|
+
*/
|
|
5849
|
+
const hydrationCache = {
|
|
5850
|
+
${hydrationCacheFields}
|
|
5851
|
+
};
|
|
5852
|
+
|
|
5853
|
+
/**
|
|
5854
|
+
* Populate the {@link hydrationCache} from the full set of every entity. Await
|
|
5855
|
+
* (or fire-and-forget) once at app start so off-page FK resolution is correct.
|
|
5856
|
+
* Idempotent \u2014 re-running refreshes every cache map.
|
|
5857
|
+
*/
|
|
5858
|
+
export async function hydrateResolverCache(): Promise<void> {
|
|
5859
|
+
await Promise.all([
|
|
5860
|
+
${hydrationCalls}
|
|
5861
|
+
]);
|
|
5862
|
+
}
|
|
5863
|
+
|
|
5724
5864
|
/**
|
|
5725
5865
|
* FK resolvers \u2014 resolve a foreign-key id to the full entity object via the
|
|
5726
|
-
* backing collection's local state (\`O(1)\` \`Map.get\`)
|
|
5866
|
+
* backing collection's local state (\`O(1)\` \`Map.get\`), falling back to the
|
|
5867
|
+
* full-fetch hydration cache for ids not on the current page.
|
|
5727
5868
|
*
|
|
5728
5869
|
* Usage:
|
|
5729
5870
|
* const ${entities[0]?.camelName ?? "thing"} = resolvers.${entities[0]?.camelName ?? "thing"}(other.${entities[0]?.camelName ?? "thing"}Id);
|
|
@@ -5732,7 +5873,7 @@ export interface Resolvers {
|
|
|
5732
5873
|
${resolverIface}
|
|
5733
5874
|
}
|
|
5734
5875
|
|
|
5735
|
-
/** Build the resolver table over the generated collections. */
|
|
5876
|
+
/** Build the resolver table over the generated collections + hydration cache. */
|
|
5736
5877
|
export function createResolvers(): Resolvers {
|
|
5737
5878
|
return {
|
|
5738
5879
|
${resolverImpls}
|
|
@@ -5744,6 +5885,7 @@ ${refsSection}`;
|
|
|
5744
5885
|
function buildLookupsFile(ctx) {
|
|
5745
5886
|
const entities = sortEntities(ctx.entities);
|
|
5746
5887
|
const collectionImports = entities.map((e) => `import { ${e.camelName}Collection } from '../collections/${e.name}';`).join("\n");
|
|
5888
|
+
const apiImports = entities.map((e) => `import { ${e.camelName}Api } from '../api/${e.name}';`).join("\n");
|
|
5747
5889
|
const typeImports = entities.map((e) => `import type { ${e.className} } from '${ctx.config.dbEntitiesImport}/${e.name}';`).join("\n");
|
|
5748
5890
|
const lookupIface = entities.map((e) => ` ${e.plural}: Map<string, ${e.className}>;`).join("\n");
|
|
5749
5891
|
const lookupBuild = entities.map(
|
|
@@ -5754,7 +5896,12 @@ function buildLookupsFile(ctx) {
|
|
|
5754
5896
|
]),
|
|
5755
5897
|
),`
|
|
5756
5898
|
).join("\n");
|
|
5899
|
+
const lookupBuildAsyncDecls = entities.map((e) => ` ${e.camelName}Api.listAll(),`).join("\n");
|
|
5900
|
+
const lookupBuildAsyncFields = entities.map(
|
|
5901
|
+
(e, i) => ` ${e.plural}: new Map(rows[${i}].map((r) => [r.id as string, r as ${e.className}])),`
|
|
5902
|
+
).join("\n");
|
|
5757
5903
|
const body = `${collectionImports}
|
|
5904
|
+
${apiImports}
|
|
5758
5905
|
${typeImports}
|
|
5759
5906
|
|
|
5760
5907
|
/** All entity lookup maps, keyed by plural entity name (id \u2192 entity). */
|
|
@@ -5762,14 +5909,33 @@ export interface EntityLookups {
|
|
|
5762
5909
|
${lookupIface}
|
|
5763
5910
|
}
|
|
5764
5911
|
|
|
5765
|
-
/** Build fresh lookup maps from current collection state. */
|
|
5912
|
+
/** Build fresh lookup maps from current collection state (current page only). */
|
|
5766
5913
|
export function buildLookups(): EntityLookups {
|
|
5767
5914
|
return {
|
|
5768
5915
|
${lookupBuild}
|
|
5769
5916
|
};
|
|
5770
5917
|
}
|
|
5771
5918
|
|
|
5772
|
-
/**
|
|
5919
|
+
/**
|
|
5920
|
+
* Build lookup maps over the COMPLETE set (LANDMINE 1 escape hatch). Pages
|
|
5921
|
+
* through every entity's list via \`api.listAll()\` so off-page rows are present.
|
|
5922
|
+
* Prefer this over {@link buildLookups} whenever a lookup must resolve ids that
|
|
5923
|
+
* may not be on the current page.
|
|
5924
|
+
*/
|
|
5925
|
+
export async function buildLookupsAsync(): Promise<EntityLookups> {
|
|
5926
|
+
const rows = await Promise.all([
|
|
5927
|
+
${lookupBuildAsyncDecls}
|
|
5928
|
+
]);
|
|
5929
|
+
return {
|
|
5930
|
+
${lookupBuildAsyncFields}
|
|
5931
|
+
};
|
|
5932
|
+
}
|
|
5933
|
+
|
|
5934
|
+
/**
|
|
5935
|
+
* Caching lookup factory: \`build()\` (re)computes from collection state,
|
|
5936
|
+
* \`hydrate()\` (re)computes from the full-fetch escape hatch, \`current\` reads,
|
|
5937
|
+
* \`clear()\` resets.
|
|
5938
|
+
*/
|
|
5773
5939
|
export function createLookups() {
|
|
5774
5940
|
let cache: EntityLookups | null = null;
|
|
5775
5941
|
return {
|
|
@@ -5777,6 +5943,10 @@ export function createLookups() {
|
|
|
5777
5943
|
cache = buildLookups();
|
|
5778
5944
|
return cache;
|
|
5779
5945
|
},
|
|
5946
|
+
hydrate: async (): Promise<EntityLookups> => {
|
|
5947
|
+
cache = await buildLookupsAsync();
|
|
5948
|
+
return cache;
|
|
5949
|
+
},
|
|
5780
5950
|
get current(): EntityLookups | null {
|
|
5781
5951
|
return cache;
|
|
5782
5952
|
},
|
|
@@ -5792,8 +5962,8 @@ function buildStoreModuleIndexFile(ctx) {
|
|
|
5792
5962
|
const entities = sortEntities(ctx.entities);
|
|
5793
5963
|
const lines = [
|
|
5794
5964
|
"export { store, type AppStore } from './index';",
|
|
5795
|
-
"export { createResolvers, type Resolvers } from './resolvers';",
|
|
5796
|
-
"export { buildLookups, createLookups, type EntityLookups } from './lookups';"
|
|
5965
|
+
"export { createResolvers, hydrateResolverCache, type Resolvers } from './resolvers';",
|
|
5966
|
+
"export { buildLookups, buildLookupsAsync, createLookups, type EntityLookups } from './lookups';"
|
|
5797
5967
|
];
|
|
5798
5968
|
const refExports = entities.filter((e) => resolvableRels(e, ctx).length > 0).map(
|
|
5799
5969
|
(e) => `export { resolve${e.className}Refs, type ${e.className}Refs } from './resolvers';`
|
|
@@ -9400,6 +9570,13 @@ var VENDORED_RUNTIME_FILES = [
|
|
|
9400
9570
|
// Pipes — ZodValidationPipe is wired on every generated controller
|
|
9401
9571
|
// @Body() to give runtime Zod validation at the controller boundary.
|
|
9402
9572
|
{ runtime: "pipes/zod-validation.pipe.ts", target: "src/shared/pipes/zod-validation.pipe.ts" },
|
|
9573
|
+
// Pagination-by-default (Page<T> envelope, ListQuerySchema, resolveListQuery,
|
|
9574
|
+
// buildPage, opaque cursor codec) — imported by EVERY generated list
|
|
9575
|
+
// controller/dto/use-case. Vendored to `src/shared/http/page.ts` (alias
|
|
9576
|
+
// `@shared/http/page`), DISTINCT from the consumer's optional `@shared/http/
|
|
9577
|
+
// pagination` search contract so the two never collide. Package mode resolves
|
|
9578
|
+
// the same source via `@pattern-stack/codegen/runtime/http/pagination`.
|
|
9579
|
+
{ runtime: "http/pagination.ts", target: "src/shared/http/page.ts" },
|
|
9403
9580
|
// EAV helpers — referenced by generated services on `eav_value_table` entities
|
|
9404
9581
|
{ runtime: "eav-helpers.ts", target: "src/shared/eav-helpers.ts" },
|
|
9405
9582
|
// OpenAPI registry (OPENAPI-1/2) — generated modules register Zod DTOs
|