@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/{chunk-LLDJS7PJ.js → chunk-IASPGFFK.js} +4 -4
  3. package/dist/{chunk-N43D57AP.js → chunk-VCXOPBYY.js} +9 -9
  4. package/dist/{chunk-6M6LZEP6.js → chunk-VDVEGTSW.js} +4 -4
  5. package/dist/{chunk-VI2VNA6Y.js → chunk-W4JYZSQK.js} +6 -6
  6. package/dist/runtime/base-classes/index.js +17 -17
  7. package/dist/runtime/http/pagination.d.ts +151 -0
  8. package/dist/runtime/http/pagination.js +98 -0
  9. package/dist/runtime/http/pagination.js.map +1 -0
  10. package/dist/runtime/subsystems/auth/auth.module.js +1 -1
  11. package/dist/runtime/subsystems/auth/index.js +6 -6
  12. package/dist/runtime/subsystems/cache/cache.module.js +1 -1
  13. package/dist/runtime/subsystems/cache/index.js +3 -3
  14. package/dist/runtime/subsystems/events/events.module.js +2 -2
  15. package/dist/runtime/subsystems/events/index.js +4 -4
  16. package/dist/runtime/subsystems/index.js +55 -55
  17. package/dist/runtime/subsystems/integration/index.js +21 -21
  18. package/dist/runtime/subsystems/integration/integration-cursor-store.drizzle-backend.js +2 -2
  19. package/dist/runtime/subsystems/integration/integration-run-recorder.drizzle-backend.js +2 -2
  20. package/dist/runtime/subsystems/integration/integration.module.js +5 -5
  21. package/dist/runtime/subsystems/observability/index.js +3 -3
  22. package/dist/src/cli/index.js +206 -29
  23. package/dist/src/cli/index.js.map +1 -1
  24. package/dist/src/index.js +6 -6
  25. package/package.json +1 -1
  26. package/runtime/http/pagination.ts +233 -0
  27. package/templates/entity/new/clean-lite-ps/controller.ejs.t +27 -6
  28. package/templates/entity/new/clean-lite-ps/dto/list-query.ejs.t +22 -0
  29. package/templates/entity/new/clean-lite-ps/index.ejs.t +1 -0
  30. package/templates/entity/new/clean-lite-ps/prompt-extension.js +14 -0
  31. package/templates/entity/new/clean-lite-ps/use-cases/list.ejs.t +56 -3
  32. package/templates/entity/new/prompt.js +17 -0
  33. /package/dist/{chunk-LLDJS7PJ.js.map → chunk-IASPGFFK.js.map} +0 -0
  34. /package/dist/{chunk-N43D57AP.js.map → chunk-VCXOPBYY.js.map} +0 -0
  35. /package/dist/{chunk-6M6LZEP6.js.map → chunk-VDVEGTSW.js.map} +0 -0
  36. /package/dist/{chunk-VI2VNA6Y.js.map → chunk-W4JYZSQK.js.map} +0 -0
@@ -46,13 +46,16 @@ import {
46
46
  writeManifest
47
47
  } from "../../chunk-K4BQQ2NN.js";
48
48
  import "../../chunk-KVOWSC5S.js";
49
- import "../../chunk-N43D57AP.js";
49
+ import "../../chunk-VCXOPBYY.js";
50
50
  import "../../chunk-PRWIX6UW.js";
51
- import "../../chunk-VI2VNA6Y.js";
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-LLDJS7PJ.js";
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, n) {
3861
- const hasUserIdField = input.copyThroughFields.some(
3862
- (f) => f.camelName === USER_ID_FIELD
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 ${n.projectionType} to carry \`${fk.writeKey}\`):`,
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 sinkInput = buildSinkInput(def, surface, slugs[0], loc.repoImportSpecifier);
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
- list: (): Promise<${className}[]> => request<${className}[]>('GET', '/${plural}'),
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
- queryFn: () => ${camelName}Api.list(),
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 collections are keyed by their plural name:
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.build()
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) as ${e.className} | undefined;
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
- /** Caching lookup factory: \`build()\` (re)computes, \`current\` reads, \`clear()\` resets. */
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