@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
package/CHANGELOG.md CHANGED
@@ -2,6 +2,37 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.26.1] — 2026-06-07
6
+
7
+ ### Fixed
8
+
9
+ - **Sink-seam emitter (#491 Shape C): generated `*.sink.generated.ts` bases now
10
+ compile for the relative-path (swe-brain) layout.** Two defects made every
11
+ emitted base non-compiling when no tsconfig alias covers the module dir
12
+ (#528):
13
+ - **Repo import one level too deep.** The sink base lives at
14
+ `integrations/<surface>/sinks/` (3 deep under `src/`) but reused the
15
+ *assembly*-relative `repoImportSpecifier` (computed for
16
+ `integrations/<surface>/modules/<provider>/`, 4 deep), emitting
17
+ `../../../../modules/…` — which lands at the repo root, not `src/` (TS2307).
18
+ The caller now recomputes the specifier relative to the SINK dir from the
19
+ resolved repo file path (alias-aware via the same `toImportSpecifier`
20
+ helper, now exported; `EntityModuleLocation` carries `repoFileAbs` /
21
+ `moduleFileAbs`). Correct prefix: `../../../modules/…`. The assembly import
22
+ depth is unchanged.
23
+ - **Bare `userId,` shorthand (TS18004).** `default<E>BuildWrite(record)` is a
24
+ standalone function with only `record` in scope, but a declared `user_id`
25
+ field was special-cased as a bare `userId,` shorthand referencing no
26
+ binding. `userId` is a plain projection field — now emitted as
27
+ `userId: record.userId`, like every other copy-through field.
28
+ - Validation: a new compile-level gate
29
+ (`sink-swe-brain-layout.compile.test.ts`) runs the real emitter into a
30
+ swe-brain-shaped tree (`integrations/<surface>/sinks/` +
31
+ `modules/<plural>/<entity>.repository.ts`, no alias) and `tsc --noEmit`s the
32
+ emitted base — the prior §3b gate missed both (it stubbed the repo import
33
+ same-dir and used no `user_id` field). Found consuming 0.26.0 in swe-brain
34
+ (6 bases, 12 errors).
35
+
5
36
  ## [0.26.0] — 2026-06-07
6
37
 
7
38
  ### Added
@@ -1,12 +1,12 @@
1
- import {
2
- integrationSubscriptions
3
- } from "./chunk-HNWZFNKP.js";
4
1
  import {
5
2
  INTEGRATION_MULTI_TENANT
6
3
  } from "./chunk-S5G3HO7N.js";
7
4
  import {
8
5
  assertTenantId
9
6
  } from "./chunk-MZ6GV4YF.js";
7
+ import {
8
+ integrationSubscriptions
9
+ } from "./chunk-HNWZFNKP.js";
10
10
  import {
11
11
  DRIZZLE
12
12
  } from "./chunk-U64T4YZE.js";
@@ -97,4 +97,4 @@ PostgresCursorStore = __decorateClass([
97
97
  export {
98
98
  PostgresCursorStore
99
99
  };
100
- //# sourceMappingURL=chunk-LLDJS7PJ.js.map
100
+ //# sourceMappingURL=chunk-IASPGFFK.js.map
@@ -1,18 +1,12 @@
1
1
  import {
2
2
  DrizzleIntegrationRunRecorder
3
- } from "./chunk-VI2VNA6Y.js";
3
+ } from "./chunk-W4JYZSQK.js";
4
4
  import {
5
5
  MemoryRunRecorder
6
6
  } from "./chunk-EO2QPOKH.js";
7
7
  import {
8
8
  PostgresCursorStore
9
- } from "./chunk-LLDJS7PJ.js";
10
- import {
11
- MemoryCursorStore
12
- } from "./chunk-AHV4GDYM.js";
13
- import {
14
- DeepEqualDiffer
15
- } from "./chunk-JEINYUJH.js";
9
+ } from "./chunk-IASPGFFK.js";
16
10
  import {
17
11
  INTEGRATION_CURSOR_STORE,
18
12
  INTEGRATION_FIELD_DIFFER,
@@ -20,6 +14,12 @@ import {
20
14
  INTEGRATION_MULTI_TENANT,
21
15
  INTEGRATION_RUN_RECORDER
22
16
  } from "./chunk-S5G3HO7N.js";
17
+ import {
18
+ MemoryCursorStore
19
+ } from "./chunk-AHV4GDYM.js";
20
+ import {
21
+ DeepEqualDiffer
22
+ } from "./chunk-JEINYUJH.js";
23
23
  import {
24
24
  __decorateClass
25
25
  } from "./chunk-2E224ZSN.js";
@@ -84,4 +84,4 @@ IntegrationModule = __decorateClass([
84
84
  export {
85
85
  IntegrationModule
86
86
  };
87
- //# sourceMappingURL=chunk-N43D57AP.js.map
87
+ //# sourceMappingURL=chunk-VCXOPBYY.js.map
@@ -1,6 +1,9 @@
1
1
  import {
2
2
  TypedEventBus
3
3
  } from "./chunk-PBENHIN2.js";
4
+ import {
5
+ MemoryEventBus
6
+ } from "./chunk-GOO5ZMYO.js";
4
7
  import {
5
8
  EventScheduler,
6
9
  scheduledEventsFromRegistry
@@ -8,9 +11,6 @@ import {
8
11
  import {
9
12
  DrizzleEventBus
10
13
  } from "./chunk-PNCOUFFI.js";
11
- import {
12
- MemoryEventBus
13
- } from "./chunk-GOO5ZMYO.js";
14
14
  import {
15
15
  EVENTS_MODULE_OPTIONS,
16
16
  EVENTS_MULTI_TENANT,
@@ -200,4 +200,4 @@ export {
200
200
  EventSchedulerLifecycle,
201
201
  EventsModule
202
202
  };
203
- //# sourceMappingURL=chunk-6M6LZEP6.js.map
203
+ //# sourceMappingURL=chunk-VDVEGTSW.js.map
@@ -1,17 +1,17 @@
1
1
  import {
2
2
  FieldDiffSchema
3
3
  } from "./chunk-SQDOBLBP.js";
4
- import {
5
- integrationRunItems,
6
- integrationRuns,
7
- integrationSubscriptions
8
- } from "./chunk-HNWZFNKP.js";
9
4
  import {
10
5
  INTEGRATION_MULTI_TENANT
11
6
  } from "./chunk-S5G3HO7N.js";
12
7
  import {
13
8
  assertTenantId
14
9
  } from "./chunk-MZ6GV4YF.js";
10
+ import {
11
+ integrationRunItems,
12
+ integrationRuns,
13
+ integrationSubscriptions
14
+ } from "./chunk-HNWZFNKP.js";
15
15
  import {
16
16
  DRIZZLE
17
17
  } from "./chunk-U64T4YZE.js";
@@ -127,4 +127,4 @@ DrizzleIntegrationRunRecorder = __decorateClass([
127
127
  export {
128
128
  DrizzleIntegrationRunRecorder
129
129
  };
130
- //# sourceMappingURL=chunk-VI2VNA6Y.js.map
130
+ //# sourceMappingURL=chunk-W4JYZSQK.js.map
@@ -1,8 +1,3 @@
1
- import {
2
- JunctionIntegrationRepository,
3
- buildCompositeExternalId,
4
- parseCompositeExternalId
5
- } from "../../chunk-2FTZLDBP.js";
6
1
  import {
7
2
  KnowledgeEntityRepository
8
3
  } from "../../chunk-NN7XZEGF.js";
@@ -18,9 +13,6 @@ import {
18
13
  import {
19
14
  WithAnalytics
20
15
  } from "../../chunk-IBGER4YK.js";
21
- import {
22
- ActivityEntityService
23
- } from "../../chunk-SJGEBMJT.js";
24
16
  import {
25
17
  BaseFindByIdUseCase,
26
18
  BaseListUseCase
@@ -32,15 +24,10 @@ import {
32
24
  IntegratedEntityService
33
25
  } from "../../chunk-5AAA4LTE.js";
34
26
  import {
35
- BaseService
36
- } from "../../chunk-BPYZCEHS.js";
37
- import {
38
- buildChangeEvents,
39
- buildLifecycleEvent,
40
- diffSnapshots,
41
- emitSafely,
42
- entitySnapshot
43
- } from "../../chunk-DAHWN63L.js";
27
+ JunctionIntegrationRepository,
28
+ buildCompositeExternalId,
29
+ parseCompositeExternalId
30
+ } from "../../chunk-2FTZLDBP.js";
44
31
  import {
45
32
  ActivityEntityRepository
46
33
  } from "../../chunk-MKWQKKK7.js";
@@ -56,6 +43,19 @@ import {
56
43
  withSuperuserScope,
57
44
  withUserScope
58
45
  } from "../../chunk-ZUKFQL6E.js";
46
+ import {
47
+ ActivityEntityService
48
+ } from "../../chunk-SJGEBMJT.js";
49
+ import {
50
+ BaseService
51
+ } from "../../chunk-BPYZCEHS.js";
52
+ import {
53
+ buildChangeEvents,
54
+ buildLifecycleEvent,
55
+ diffSnapshots,
56
+ emitSafely,
57
+ entitySnapshot
58
+ } from "../../chunk-DAHWN63L.js";
59
59
  import "../../chunk-2E224ZSN.js";
60
60
  export {
61
61
  ActivityEntityRepository,
@@ -0,0 +1,151 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Pagination envelope + ListQuery contract for generated entity list endpoints
5
+ * (pagination-by-default).
6
+ *
7
+ * Every generated `GET /<entities>` returns a {@link Page} envelope instead of
8
+ * a bare `T[]`. The request shape is {@link ListQuery} (page/cursor/pageSize +
9
+ * default sort) merged with arbitrary where-filters at the controller.
10
+ *
11
+ * Mirrors the in-repo precedent: the jobs subsystem's `JobRunPage`
12
+ * (`{ items, nextCursor }`) and its opaque keyset cursor codec
13
+ * (`runtime/subsystems/jobs/job-run-keyset-cursor.ts`). The entity envelope
14
+ * EXTENDS that minimal shape with `page/pageCount/total/pageSize` so a numbered
15
+ * UI (jump-to-page) works while `nextCursor` stays contract-stable for the
16
+ * later keyset upgrade.
17
+ *
18
+ * ENGINE NOTE (v1): the list use-case fetches by OFFSET (page-based). The
19
+ * `nextCursor` is computed from the last row and emitted from day one so the
20
+ * contract never changes, but cursor-REQUEST honoring (keyset seek) is a
21
+ * DEFERRED seam — see the TODO in the generated list use-case / the repository
22
+ * query branch. `ListQuery.cursor` is ACCEPTED (no validation error) even
23
+ * though v1 ignores it for fetching.
24
+ */
25
+
26
+ /** Default page size when `pageSize` is omitted. */
27
+ declare const DEFAULT_PAGE_SIZE = 50;
28
+ /** Hard upper bound on page size to keep a single read bounded. */
29
+ declare const MAX_PAGE_SIZE = 200;
30
+ /** Default page (1-based) when `page` is omitted. */
31
+ declare const DEFAULT_PAGE = 1;
32
+ /** Default sort column. */
33
+ declare const DEFAULT_SORT_BY = "created_at";
34
+ /** Default sort direction. */
35
+ declare const DEFAULT_SORT_ORDER: "desc";
36
+ /** Clamp a caller-supplied `pageSize` into `[1, MAX_PAGE_SIZE]`. */
37
+ declare function clampPageSize(pageSize: number | undefined): number;
38
+ /** Clamp a caller-supplied `page` to a 1-based integer (floor 1). */
39
+ declare function clampPage(page: number | undefined): number;
40
+ /**
41
+ * Zod schema for the universal list query string. All keys optional —
42
+ * pagination works fully UNFILTERED (the default mode). `pageSize` is clamped
43
+ * (default 50, max 200) and `sort_order` defaults to `desc`. Arbitrary where
44
+ * filters are NOT modeled here (they're parsed/passed through at the controller
45
+ * via `.passthrough()`); this schema owns ONLY the pagination + sort knobs so
46
+ * the defaults + clamp land in one place.
47
+ *
48
+ * `cursor` is accepted but v1 ignores it for fetching (offset engine) — the
49
+ * keyset seek is the deferred seam. Passing a `nextCursor` back never errors.
50
+ */
51
+ declare const ListQuerySchema: z.ZodObject<{
52
+ page: z.ZodOptional<z.ZodNumber>;
53
+ cursor: z.ZodOptional<z.ZodString>;
54
+ pageSize: z.ZodOptional<z.ZodNumber>;
55
+ sort_by: z.ZodOptional<z.ZodString>;
56
+ sort_order: z.ZodOptional<z.ZodEnum<["asc", "desc"]>>;
57
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
58
+ page: z.ZodOptional<z.ZodNumber>;
59
+ cursor: z.ZodOptional<z.ZodString>;
60
+ pageSize: z.ZodOptional<z.ZodNumber>;
61
+ sort_by: z.ZodOptional<z.ZodString>;
62
+ sort_order: z.ZodOptional<z.ZodEnum<["asc", "desc"]>>;
63
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
64
+ page: z.ZodOptional<z.ZodNumber>;
65
+ cursor: z.ZodOptional<z.ZodString>;
66
+ pageSize: z.ZodOptional<z.ZodNumber>;
67
+ sort_by: z.ZodOptional<z.ZodString>;
68
+ sort_order: z.ZodOptional<z.ZodEnum<["asc", "desc"]>>;
69
+ }, z.ZodTypeAny, "passthrough">>;
70
+ /** Parsed list query (pre-clamp). Use {@link resolveListQuery} to normalize. */
71
+ type ListQuery = z.infer<typeof ListQuerySchema>;
72
+ /**
73
+ * Normalized pagination options resolved from a raw {@link ListQuery}: clamped
74
+ * `page`/`pageSize`, computed `offset`, and a defaulted sort. The generated
75
+ * list use-case feeds these straight into `service.list({ limit, offset, ... })`.
76
+ */
77
+ interface ResolvedListQuery {
78
+ page: number;
79
+ pageSize: number;
80
+ /** `(page - 1) * pageSize`. */
81
+ offset: number;
82
+ sortBy: string;
83
+ sortOrder: 'asc' | 'desc';
84
+ /** Opaque cursor as passed by the caller (v1: not honored — deferred seam). */
85
+ cursor?: string;
86
+ }
87
+ /**
88
+ * Resolve a raw list query into normalized, clamped pagination options.
89
+ * Defaults: page 1, pageSize 50 (max 200), sort `created_at desc`.
90
+ */
91
+ declare function resolveListQuery(query: ListQuery | undefined): ResolvedListQuery;
92
+ /**
93
+ * One page of a paginated list response. EXTENDS the jobs subsystem's minimal
94
+ * `{ items, nextCursor }` shape with the numbered-UI fields:
95
+ *
96
+ * - `page` — 1-based page number of THIS page.
97
+ * - `pageCount` — total number of pages (`ceil(total / pageSize)`, min 1).
98
+ * - `total` — total matching rows (reflects any where-filter).
99
+ * - `pageSize` — the (clamped) page size used.
100
+ * - `nextCursor` — opaque keyset cursor of the LAST row, or `null` on the
101
+ * last page / empty result. Contract-stable from day one;
102
+ * v1 emits it but fetches by offset.
103
+ */
104
+ interface Page<T> {
105
+ items: T[];
106
+ page: number;
107
+ pageCount: number;
108
+ total: number;
109
+ pageSize: number;
110
+ nextCursor: string | null;
111
+ }
112
+ /** Keyset tuple a {@link Page.nextCursor} encodes. */
113
+ interface PageKeyset {
114
+ /** `created_at` of the last row on this page. */
115
+ createdAt: Date;
116
+ /** `id` (UUID) tie-break of the last row on this page. */
117
+ id: string;
118
+ }
119
+ /**
120
+ * Encode a `(createdAt, id)` keyset into an opaque, base64url cursor. The shape
121
+ * (a JSON tuple) is an implementation detail — never parse it outside this
122
+ * module. Mirrors `encodeKeysetCursor` in the jobs subsystem.
123
+ */
124
+ declare function encodeCursor(keyset: PageKeyset): string;
125
+ /**
126
+ * Decode an opaque cursor back into its `(createdAt, id)` keyset. Returns
127
+ * `null` for a malformed cursor so a caller can treat garbage as "start from
128
+ * the beginning" rather than throw on user-supplied data.
129
+ */
130
+ declare function decodeCursor(cursor: string): PageKeyset | null;
131
+ /**
132
+ * Compute the `nextCursor` for a page of rows: the opaque cursor of the LAST
133
+ * row when more pages remain, else `null`. A row is expected to carry
134
+ * `createdAt: Date` and `id: string` (the default-sort keyset). Rows missing
135
+ * either field yield `null` (cursor not derivable — caller falls back to offset
136
+ * paging, which is the v1 engine anyway).
137
+ *
138
+ * @param rows the items on this page (already fetched, in sort order)
139
+ * @param hasMore whether more rows exist beyond this page
140
+ * (`offset + rows.length < total`)
141
+ */
142
+ declare function computeNextCursor(rows: ReadonlyArray<unknown>, hasMore: boolean): string | null;
143
+ /**
144
+ * Assemble a {@link Page} envelope from a fetched page of rows + the total
145
+ * matching count + the resolved query. Computes `pageCount` and `nextCursor`.
146
+ * The single place the envelope shape is constructed, so the generated list
147
+ * use-case stays a thin call.
148
+ */
149
+ declare function buildPage<T>(items: T[], total: number, resolved: Pick<ResolvedListQuery, 'page' | 'pageSize' | 'offset'>): Page<T>;
150
+
151
+ export { DEFAULT_PAGE, DEFAULT_PAGE_SIZE, DEFAULT_SORT_BY, DEFAULT_SORT_ORDER, type ListQuery, ListQuerySchema, MAX_PAGE_SIZE, type Page, type PageKeyset, type ResolvedListQuery, buildPage, clampPage, clampPageSize, computeNextCursor, decodeCursor, encodeCursor, resolveListQuery };
@@ -0,0 +1,98 @@
1
+ import "../../chunk-2E224ZSN.js";
2
+
3
+ // runtime/http/pagination.ts
4
+ import { z } from "zod";
5
+ var DEFAULT_PAGE_SIZE = 50;
6
+ var MAX_PAGE_SIZE = 200;
7
+ var DEFAULT_PAGE = 1;
8
+ var DEFAULT_SORT_BY = "created_at";
9
+ var DEFAULT_SORT_ORDER = "desc";
10
+ function clampPageSize(pageSize) {
11
+ if (typeof pageSize !== "number" || !Number.isFinite(pageSize)) {
12
+ return DEFAULT_PAGE_SIZE;
13
+ }
14
+ const floored = Math.floor(pageSize);
15
+ if (floored < 1) return 1;
16
+ if (floored > MAX_PAGE_SIZE) return MAX_PAGE_SIZE;
17
+ return floored;
18
+ }
19
+ function clampPage(page) {
20
+ if (typeof page !== "number" || !Number.isFinite(page)) {
21
+ return DEFAULT_PAGE;
22
+ }
23
+ const floored = Math.floor(page);
24
+ return floored < 1 ? 1 : floored;
25
+ }
26
+ var ListQuerySchema = z.object({
27
+ page: z.coerce.number().int().optional(),
28
+ cursor: z.string().optional(),
29
+ pageSize: z.coerce.number().int().optional(),
30
+ sort_by: z.string().optional(),
31
+ sort_order: z.enum(["asc", "desc"]).optional()
32
+ }).passthrough();
33
+ function resolveListQuery(query) {
34
+ const page = clampPage(query?.page);
35
+ const pageSize = clampPageSize(query?.pageSize);
36
+ return {
37
+ page,
38
+ pageSize,
39
+ offset: (page - 1) * pageSize,
40
+ sortBy: query?.sort_by ?? DEFAULT_SORT_BY,
41
+ sortOrder: query?.sort_order ?? DEFAULT_SORT_ORDER,
42
+ cursor: query?.cursor
43
+ };
44
+ }
45
+ function encodeCursor(keyset) {
46
+ const tuple = [keyset.createdAt.toISOString(), keyset.id];
47
+ return Buffer.from(JSON.stringify(tuple), "utf8").toString("base64url");
48
+ }
49
+ function decodeCursor(cursor) {
50
+ try {
51
+ const json = Buffer.from(cursor, "base64url").toString("utf8");
52
+ const parsed = JSON.parse(json);
53
+ if (!Array.isArray(parsed) || parsed.length !== 2) return null;
54
+ const [iso, id] = parsed;
55
+ if (typeof iso !== "string" || typeof id !== "string") return null;
56
+ const createdAt = new Date(iso);
57
+ if (Number.isNaN(createdAt.getTime())) return null;
58
+ return { createdAt, id };
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+ function computeNextCursor(rows, hasMore) {
64
+ if (!hasMore || rows.length === 0) return null;
65
+ const last = rows[rows.length - 1];
66
+ const createdAt = last?.createdAt;
67
+ const id = last?.id;
68
+ if (!(createdAt instanceof Date) || typeof id !== "string") return null;
69
+ return encodeCursor({ createdAt, id });
70
+ }
71
+ function buildPage(items, total, resolved) {
72
+ const pageCount = total === 0 ? 1 : Math.ceil(total / resolved.pageSize);
73
+ const hasMore = resolved.offset + items.length < total;
74
+ return {
75
+ items,
76
+ page: resolved.page,
77
+ pageCount,
78
+ total,
79
+ pageSize: resolved.pageSize,
80
+ nextCursor: computeNextCursor(items, hasMore)
81
+ };
82
+ }
83
+ export {
84
+ DEFAULT_PAGE,
85
+ DEFAULT_PAGE_SIZE,
86
+ DEFAULT_SORT_BY,
87
+ DEFAULT_SORT_ORDER,
88
+ ListQuerySchema,
89
+ MAX_PAGE_SIZE,
90
+ buildPage,
91
+ clampPage,
92
+ clampPageSize,
93
+ computeNextCursor,
94
+ decodeCursor,
95
+ encodeCursor,
96
+ resolveListQuery
97
+ };
98
+ //# sourceMappingURL=pagination.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../runtime/http/pagination.ts"],"sourcesContent":["/**\n * Pagination envelope + ListQuery contract for generated entity list endpoints\n * (pagination-by-default).\n *\n * Every generated `GET /<entities>` returns a {@link Page} envelope instead of\n * a bare `T[]`. The request shape is {@link ListQuery} (page/cursor/pageSize +\n * default sort) merged with arbitrary where-filters at the controller.\n *\n * Mirrors the in-repo precedent: the jobs subsystem's `JobRunPage`\n * (`{ items, nextCursor }`) and its opaque keyset cursor codec\n * (`runtime/subsystems/jobs/job-run-keyset-cursor.ts`). The entity envelope\n * EXTENDS that minimal shape with `page/pageCount/total/pageSize` so a numbered\n * UI (jump-to-page) works while `nextCursor` stays contract-stable for the\n * later keyset upgrade.\n *\n * ENGINE NOTE (v1): the list use-case fetches by OFFSET (page-based). The\n * `nextCursor` is computed from the last row and emitted from day one so the\n * contract never changes, but cursor-REQUEST honoring (keyset seek) is a\n * DEFERRED seam — see the TODO in the generated list use-case / the repository\n * query branch. `ListQuery.cursor` is ACCEPTED (no validation error) even\n * though v1 ignores it for fetching.\n */\n\nimport { z } from 'zod';\n\n// ============================================================================\n// Defaults + clamp\n// ============================================================================\n\n/** Default page size when `pageSize` is omitted. */\nexport const DEFAULT_PAGE_SIZE = 50;\n/** Hard upper bound on page size to keep a single read bounded. */\nexport const MAX_PAGE_SIZE = 200;\n/** Default page (1-based) when `page` is omitted. */\nexport const DEFAULT_PAGE = 1;\n/** Default sort column. */\nexport const DEFAULT_SORT_BY = 'created_at';\n/** Default sort direction. */\nexport const DEFAULT_SORT_ORDER = 'desc' as const;\n\n/** Clamp a caller-supplied `pageSize` into `[1, MAX_PAGE_SIZE]`. */\nexport function clampPageSize(pageSize: number | undefined): number {\n if (typeof pageSize !== 'number' || !Number.isFinite(pageSize)) {\n return DEFAULT_PAGE_SIZE;\n }\n const floored = Math.floor(pageSize);\n if (floored < 1) return 1;\n if (floored > MAX_PAGE_SIZE) return MAX_PAGE_SIZE;\n return floored;\n}\n\n/** Clamp a caller-supplied `page` to a 1-based integer (floor 1). */\nexport function clampPage(page: number | undefined): number {\n if (typeof page !== 'number' || !Number.isFinite(page)) {\n return DEFAULT_PAGE;\n }\n const floored = Math.floor(page);\n return floored < 1 ? 1 : floored;\n}\n\n// ============================================================================\n// ListQuery schema (request)\n// ============================================================================\n\n/**\n * Zod schema for the universal list query string. All keys optional —\n * pagination works fully UNFILTERED (the default mode). `pageSize` is clamped\n * (default 50, max 200) and `sort_order` defaults to `desc`. Arbitrary where\n * filters are NOT modeled here (they're parsed/passed through at the controller\n * via `.passthrough()`); this schema owns ONLY the pagination + sort knobs so\n * the defaults + clamp land in one place.\n *\n * `cursor` is accepted but v1 ignores it for fetching (offset engine) — the\n * keyset seek is the deferred seam. Passing a `nextCursor` back never errors.\n */\nexport const ListQuerySchema = z\n .object({\n page: z.coerce.number().int().optional(),\n cursor: z.string().optional(),\n pageSize: z.coerce.number().int().optional(),\n sort_by: z.string().optional(),\n sort_order: z.enum(['asc', 'desc']).optional(),\n })\n .passthrough();\n\n/** Parsed list query (pre-clamp). Use {@link resolveListQuery} to normalize. */\nexport type ListQuery = z.infer<typeof ListQuerySchema>;\n\n/**\n * Normalized pagination options resolved from a raw {@link ListQuery}: clamped\n * `page`/`pageSize`, computed `offset`, and a defaulted sort. The generated\n * list use-case feeds these straight into `service.list({ limit, offset, ... })`.\n */\nexport interface ResolvedListQuery {\n page: number;\n pageSize: number;\n /** `(page - 1) * pageSize`. */\n offset: number;\n sortBy: string;\n sortOrder: 'asc' | 'desc';\n /** Opaque cursor as passed by the caller (v1: not honored — deferred seam). */\n cursor?: string;\n}\n\n/**\n * Resolve a raw list query into normalized, clamped pagination options.\n * Defaults: page 1, pageSize 50 (max 200), sort `created_at desc`.\n */\nexport function resolveListQuery(query: ListQuery | undefined): ResolvedListQuery {\n const page = clampPage(query?.page);\n const pageSize = clampPageSize(query?.pageSize);\n return {\n page,\n pageSize,\n offset: (page - 1) * pageSize,\n sortBy: query?.sort_by ?? DEFAULT_SORT_BY,\n sortOrder: query?.sort_order ?? DEFAULT_SORT_ORDER,\n cursor: query?.cursor,\n };\n}\n\n// ============================================================================\n// Page envelope (response)\n// ============================================================================\n\n/**\n * One page of a paginated list response. EXTENDS the jobs subsystem's minimal\n * `{ items, nextCursor }` shape with the numbered-UI fields:\n *\n * - `page` — 1-based page number of THIS page.\n * - `pageCount` — total number of pages (`ceil(total / pageSize)`, min 1).\n * - `total` — total matching rows (reflects any where-filter).\n * - `pageSize` — the (clamped) page size used.\n * - `nextCursor` — opaque keyset cursor of the LAST row, or `null` on the\n * last page / empty result. Contract-stable from day one;\n * v1 emits it but fetches by offset.\n */\nexport interface Page<T> {\n items: T[];\n page: number;\n pageCount: number;\n total: number;\n pageSize: number;\n nextCursor: string | null;\n}\n\n// ============================================================================\n// Opaque cursor codec (encodes (createdAt, id))\n// ============================================================================\n\n/** Keyset tuple a {@link Page.nextCursor} encodes. */\nexport interface PageKeyset {\n /** `created_at` of the last row on this page. */\n createdAt: Date;\n /** `id` (UUID) tie-break of the last row on this page. */\n id: string;\n}\n\n/**\n * Encode a `(createdAt, id)` keyset into an opaque, base64url cursor. The shape\n * (a JSON tuple) is an implementation detail — never parse it outside this\n * module. Mirrors `encodeKeysetCursor` in the jobs subsystem.\n */\nexport function encodeCursor(keyset: PageKeyset): string {\n const tuple = [keyset.createdAt.toISOString(), keyset.id];\n return Buffer.from(JSON.stringify(tuple), 'utf8').toString('base64url');\n}\n\n/**\n * Decode an opaque cursor back into its `(createdAt, id)` keyset. Returns\n * `null` for a malformed cursor so a caller can treat garbage as \"start from\n * the beginning\" rather than throw on user-supplied data.\n */\nexport function decodeCursor(cursor: string): PageKeyset | null {\n try {\n const json = Buffer.from(cursor, 'base64url').toString('utf8');\n const parsed = JSON.parse(json) as unknown;\n if (!Array.isArray(parsed) || parsed.length !== 2) return null;\n const [iso, id] = parsed;\n if (typeof iso !== 'string' || typeof id !== 'string') return null;\n const createdAt = new Date(iso);\n if (Number.isNaN(createdAt.getTime())) return null;\n return { createdAt, id };\n } catch {\n return null;\n }\n}\n\n/**\n * Compute the `nextCursor` for a page of rows: the opaque cursor of the LAST\n * row when more pages remain, else `null`. A row is expected to carry\n * `createdAt: Date` and `id: string` (the default-sort keyset). Rows missing\n * either field yield `null` (cursor not derivable — caller falls back to offset\n * paging, which is the v1 engine anyway).\n *\n * @param rows the items on this page (already fetched, in sort order)\n * @param hasMore whether more rows exist beyond this page\n * (`offset + rows.length < total`)\n */\nexport function computeNextCursor(\n rows: ReadonlyArray<unknown>,\n hasMore: boolean,\n): string | null {\n if (!hasMore || rows.length === 0) return null;\n const last = rows[rows.length - 1] as { createdAt?: unknown; id?: unknown };\n const createdAt = last?.createdAt;\n const id = last?.id;\n if (!(createdAt instanceof Date) || typeof id !== 'string') return null;\n return encodeCursor({ createdAt, id });\n}\n\n/**\n * Assemble a {@link Page} envelope from a fetched page of rows + the total\n * matching count + the resolved query. Computes `pageCount` and `nextCursor`.\n * The single place the envelope shape is constructed, so the generated list\n * use-case stays a thin call.\n */\nexport function buildPage<T>(\n items: T[],\n total: number,\n resolved: Pick<ResolvedListQuery, 'page' | 'pageSize' | 'offset'>,\n): Page<T> {\n const pageCount = total === 0 ? 1 : Math.ceil(total / resolved.pageSize);\n const hasMore = resolved.offset + items.length < total;\n return {\n items,\n page: resolved.page,\n pageCount,\n total,\n pageSize: resolved.pageSize,\n nextCursor: computeNextCursor(items as ReadonlyArray<unknown>, hasMore),\n };\n}\n"],"mappings":";;;AAuBA,SAAS,SAAS;AAOX,IAAM,oBAAoB;AAE1B,IAAM,gBAAgB;AAEtB,IAAM,eAAe;AAErB,IAAM,kBAAkB;AAExB,IAAM,qBAAqB;AAG3B,SAAS,cAAc,UAAsC;AAClE,MAAI,OAAO,aAAa,YAAY,CAAC,OAAO,SAAS,QAAQ,GAAG;AAC9D,WAAO;AAAA,EACT;AACA,QAAM,UAAU,KAAK,MAAM,QAAQ;AACnC,MAAI,UAAU,EAAG,QAAO;AACxB,MAAI,UAAU,cAAe,QAAO;AACpC,SAAO;AACT;AAGO,SAAS,UAAU,MAAkC;AAC1D,MAAI,OAAO,SAAS,YAAY,CAAC,OAAO,SAAS,IAAI,GAAG;AACtD,WAAO;AAAA,EACT;AACA,QAAM,UAAU,KAAK,MAAM,IAAI;AAC/B,SAAO,UAAU,IAAI,IAAI;AAC3B;AAiBO,IAAM,kBAAkB,EAC5B,OAAO;AAAA,EACN,MAAM,EAAE,OAAO,OAAO,EAAE,IAAI,EAAE,SAAS;AAAA,EACvC,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,UAAU,EAAE,OAAO,OAAO,EAAE,IAAI,EAAE,SAAS;AAAA,EAC3C,SAAS,EAAE,OAAO,EAAE,SAAS;AAAA,EAC7B,YAAY,EAAE,KAAK,CAAC,OAAO,MAAM,CAAC,EAAE,SAAS;AAC/C,CAAC,EACA,YAAY;AAyBR,SAAS,iBAAiB,OAAiD;AAChF,QAAM,OAAO,UAAU,OAAO,IAAI;AAClC,QAAM,WAAW,cAAc,OAAO,QAAQ;AAC9C,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,SAAS,OAAO,KAAK;AAAA,IACrB,QAAQ,OAAO,WAAW;AAAA,IAC1B,WAAW,OAAO,cAAc;AAAA,IAChC,QAAQ,OAAO;AAAA,EACjB;AACF;AA4CO,SAAS,aAAa,QAA4B;AACvD,QAAM,QAAQ,CAAC,OAAO,UAAU,YAAY,GAAG,OAAO,EAAE;AACxD,SAAO,OAAO,KAAK,KAAK,UAAU,KAAK,GAAG,MAAM,EAAE,SAAS,WAAW;AACxE;AAOO,SAAS,aAAa,QAAmC;AAC9D,MAAI;AACF,UAAM,OAAO,OAAO,KAAK,QAAQ,WAAW,EAAE,SAAS,MAAM;AAC7D,UAAM,SAAS,KAAK,MAAM,IAAI;AAC9B,QAAI,CAAC,MAAM,QAAQ,MAAM,KAAK,OAAO,WAAW,EAAG,QAAO;AAC1D,UAAM,CAAC,KAAK,EAAE,IAAI;AAClB,QAAI,OAAO,QAAQ,YAAY,OAAO,OAAO,SAAU,QAAO;AAC9D,UAAM,YAAY,IAAI,KAAK,GAAG;AAC9B,QAAI,OAAO,MAAM,UAAU,QAAQ,CAAC,EAAG,QAAO;AAC9C,WAAO,EAAE,WAAW,GAAG;AAAA,EACzB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAaO,SAAS,kBACd,MACA,SACe;AACf,MAAI,CAAC,WAAW,KAAK,WAAW,EAAG,QAAO;AAC1C,QAAM,OAAO,KAAK,KAAK,SAAS,CAAC;AACjC,QAAM,YAAY,MAAM;AACxB,QAAM,KAAK,MAAM;AACjB,MAAI,EAAE,qBAAqB,SAAS,OAAO,OAAO,SAAU,QAAO;AACnE,SAAO,aAAa,EAAE,WAAW,GAAG,CAAC;AACvC;AAQO,SAAS,UACd,OACA,OACA,UACS;AACT,QAAM,YAAY,UAAU,IAAI,IAAI,KAAK,KAAK,QAAQ,SAAS,QAAQ;AACvE,QAAM,UAAU,SAAS,SAAS,MAAM,SAAS;AACjD,SAAO;AAAA,IACL;AAAA,IACA,MAAM,SAAS;AAAA,IACf;AAAA,IACA;AAAA,IACA,UAAU,SAAS;AAAA,IACnB,YAAY,kBAAkB,OAAiC,OAAO;AAAA,EACxE;AACF;","names":[]}
@@ -6,8 +6,8 @@ import "../../../chunk-N5OTOWTP.js";
6
6
  import "../../../chunk-QLTJSCE6.js";
7
7
  import "../../../chunk-BPARRK6F.js";
8
8
  import "../../../chunk-SZVPIHWE.js";
9
- import "../../../chunk-NPFPZ2HO.js";
10
9
  import "../../../chunk-6XY6ZMMD.js";
10
+ import "../../../chunk-NPFPZ2HO.js";
11
11
  import "../../../chunk-GYGNEQSC.js";
12
12
  import "../../../chunk-U64T4YZE.js";
13
13
  import "../../../chunk-2E224ZSN.js";
@@ -1,13 +1,13 @@
1
1
  import "../../../chunk-7C3FOSDI.js";
2
+ import {
3
+ withAuthRetry
4
+ } from "../../../chunk-WL67FZGF.js";
2
5
  import {
3
6
  OAuth2RefreshStrategy
4
7
  } from "../../../chunk-M6QLSLPO.js";
5
8
  import {
6
9
  ConnectionBrokenError
7
10
  } from "../../../chunk-2N4UG4VD.js";
8
- import {
9
- withAuthRetry
10
- } from "../../../chunk-WL67FZGF.js";
11
11
  import {
12
12
  SessionExpiredError,
13
13
  isSessionExpiredError
@@ -35,9 +35,6 @@ import {
35
35
  import {
36
36
  AuthController
37
37
  } from "../../../chunk-SZVPIHWE.js";
38
- import {
39
- authOAuthState
40
- } from "../../../chunk-NPFPZ2HO.js";
41
38
  import {
42
39
  AUTH_CONNECTION_GRANT_SINK,
43
40
  AUTH_CONNECTION_READER,
@@ -48,6 +45,9 @@ import {
48
45
  OAUTH_STATE_STORE,
49
46
  STRATEGY_REGISTRY
50
47
  } from "../../../chunk-6XY6ZMMD.js";
48
+ import {
49
+ authOAuthState
50
+ } from "../../../chunk-NPFPZ2HO.js";
51
51
  import "../../../chunk-GYGNEQSC.js";
52
52
  import "../../../chunk-U64T4YZE.js";
53
53
  import "../../../chunk-ZUKFQL6E.js";
@@ -2,8 +2,8 @@ import {
2
2
  CacheModule
3
3
  } from "../../../chunk-COGHTKXY.js";
4
4
  import "../../../chunk-T6C4LFLC.js";
5
- import "../../../chunk-FASRXRX5.js";
6
5
  import "../../../chunk-IF5I3DAA.js";
6
+ import "../../../chunk-FASRXRX5.js";
7
7
  import "../../../chunk-L6FTY45T.js";
8
8
  import "../../../chunk-GYGNEQSC.js";
9
9
  import "../../../chunk-U64T4YZE.js";
@@ -5,12 +5,12 @@ import {
5
5
  import {
6
6
  DrizzleCacheService
7
7
  } from "../../../chunk-T6C4LFLC.js";
8
- import {
9
- cacheEntries
10
- } from "../../../chunk-FASRXRX5.js";
11
8
  import {
12
9
  MemoryCacheService
13
10
  } from "../../../chunk-IF5I3DAA.js";
11
+ import {
12
+ cacheEntries
13
+ } from "../../../chunk-FASRXRX5.js";
14
14
  import {
15
15
  CACHE,
16
16
  CACHE_DEFAULT_TTL
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  EventSchedulerLifecycle,
3
3
  EventsModule
4
- } from "../../../chunk-6M6LZEP6.js";
4
+ } from "../../../chunk-VDVEGTSW.js";
5
5
  import "../../../chunk-PBENHIN2.js";
6
6
  import "../../../chunk-LQZESSM3.js";
7
7
  import "../../../chunk-MU54DZCC.js";
8
+ import "../../../chunk-GOO5ZMYO.js";
8
9
  import "../../../chunk-DUUCU77W.js";
9
10
  import "../../../chunk-DUBZOXJC.js";
10
11
  import "../../../chunk-PNCOUFFI.js";
11
- import "../../../chunk-GOO5ZMYO.js";
12
12
  import "../../../chunk-UQ5EHOH2.js";
13
13
  import "../../../chunk-Q6LRJ4VI.js";
14
14
  import "../../../chunk-H5NH7KPE.js";
@@ -2,12 +2,15 @@ import "../../../chunk-SYVZ4MD2.js";
2
2
  import {
3
3
  EventSchedulerLifecycle,
4
4
  EventsModule
5
- } from "../../../chunk-6M6LZEP6.js";
5
+ } from "../../../chunk-VDVEGTSW.js";
6
6
  import {
7
7
  TypedEventBus
8
8
  } from "../../../chunk-PBENHIN2.js";
9
9
  import "../../../chunk-LQZESSM3.js";
10
10
  import "../../../chunk-MU54DZCC.js";
11
+ import {
12
+ MemoryEventBus
13
+ } from "../../../chunk-GOO5ZMYO.js";
11
14
  import {
12
15
  EventScheduler,
13
16
  SCHEDULE_FLOOR_MS,
@@ -26,9 +29,6 @@ import {
26
29
  import {
27
30
  DrizzleEventBus
28
31
  } from "../../../chunk-PNCOUFFI.js";
29
- import {
30
- MemoryEventBus
31
- } from "../../../chunk-GOO5ZMYO.js";
32
32
  import "../../../chunk-UQ5EHOH2.js";
33
33
  import "../../../chunk-Q6LRJ4VI.js";
34
34
  import {