@classytic/repo-core 0.2.0 → 0.4.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 (87) hide show
  1. package/CHANGELOG.md +363 -0
  2. package/README.md +28 -7
  3. package/dist/adapter/index.d.mts +3 -0
  4. package/dist/adapter/index.mjs +2 -0
  5. package/dist/adapter/types.d.mts +222 -0
  6. package/dist/adapter/widen.d.mts +22 -0
  7. package/dist/adapter/widen.mjs +26 -0
  8. package/dist/aggregate/index.d.mts +3 -0
  9. package/dist/aggregate/index.mjs +3 -0
  10. package/dist/aggregate/keyset.d.mts +57 -0
  11. package/dist/aggregate/keyset.mjs +45 -0
  12. package/dist/aggregate/normalize.d.mts +24 -0
  13. package/dist/aggregate/normalize.mjs +28 -0
  14. package/dist/better-auth/index.d.mts +110 -0
  15. package/dist/better-auth/index.mjs +71 -0
  16. package/dist/cache/engine.d.mts +127 -0
  17. package/dist/cache/engine.mjs +235 -0
  18. package/dist/cache/envelope.mjs +32 -0
  19. package/dist/cache/index.d.mts +7 -2
  20. package/dist/cache/index.mjs +6 -2
  21. package/dist/cache/keys.mjs +131 -0
  22. package/dist/cache/memory-adapter.mjs +41 -7
  23. package/dist/cache/options.d.mts +112 -0
  24. package/dist/cache/options.mjs +25 -0
  25. package/dist/cache/plugin/context.d.mts +18 -0
  26. package/dist/cache/plugin/context.mjs +121 -0
  27. package/dist/cache/plugin/index.d.mts +86 -0
  28. package/dist/cache/plugin/index.mjs +78 -0
  29. package/dist/cache/plugin/invalidation-hooks.mjs +35 -0
  30. package/dist/cache/plugin/read-hooks.mjs +96 -0
  31. package/dist/cache/plugin/swr.mjs +20 -0
  32. package/dist/cache/runtime.d.mts +43 -0
  33. package/dist/cache/runtime.mjs +14 -0
  34. package/dist/cache/tag-index.mjs +84 -0
  35. package/dist/cache/timeout-adapter.d.mts +30 -0
  36. package/dist/cache/timeout-adapter.mjs +58 -0
  37. package/dist/cache/types.d.mts +45 -0
  38. package/dist/cache/version-store.mjs +57 -0
  39. package/dist/errors/contract.d.mts +37 -0
  40. package/dist/errors/contract.mjs +75 -0
  41. package/dist/errors/index.d.mts +4 -2
  42. package/dist/errors/index.mjs +4 -1
  43. package/dist/errors/schema.d.mts +101 -0
  44. package/dist/errors/schema.mjs +78 -0
  45. package/dist/errors/types.d.mts +113 -8
  46. package/dist/errors/types.mjs +29 -0
  47. package/dist/filter/match.mjs +38 -2
  48. package/dist/pagination/canonical.d.mts +35 -0
  49. package/dist/pagination/canonical.mjs +26 -0
  50. package/dist/pagination/cursor.mjs +4 -1
  51. package/dist/pagination/index.d.mts +3 -2
  52. package/dist/pagination/index.mjs +2 -1
  53. package/dist/pagination/types.d.mts +57 -3
  54. package/dist/plugins/index.d.mts +2 -0
  55. package/dist/plugins/index.mjs +2 -0
  56. package/dist/plugins/tenant-helpers.d.mts +63 -0
  57. package/dist/plugins/tenant-helpers.mjs +84 -0
  58. package/dist/query-parser/index.d.mts +2 -1
  59. package/dist/query-parser/index.mjs +2 -1
  60. package/dist/query-parser/parse-url.mjs +13 -11
  61. package/dist/query-parser/reserved.d.mts +43 -0
  62. package/dist/query-parser/reserved.mjs +56 -0
  63. package/dist/repository/agg-output.d.mts +63 -0
  64. package/dist/repository/agg-output.mjs +89 -0
  65. package/dist/repository/base.mjs +21 -0
  66. package/dist/repository/index.d.mts +4 -2
  67. package/dist/repository/index.mjs +3 -1
  68. package/dist/repository/options.d.mts +62 -0
  69. package/dist/repository/options.mjs +57 -0
  70. package/dist/repository/types.d.mts +935 -48
  71. package/dist/schema/field-rules.d.mts +60 -9
  72. package/dist/schema/field-rules.mjs +121 -10
  73. package/dist/schema/generator.d.mts +72 -0
  74. package/dist/schema/generator.mjs +16 -0
  75. package/dist/schema/index.d.mts +3 -2
  76. package/dist/schema/index.mjs +3 -2
  77. package/dist/schema/types.d.mts +77 -3
  78. package/dist/tenant/index.d.mts +3 -0
  79. package/dist/tenant/index.mjs +2 -0
  80. package/dist/tenant/resolve.d.mts +27 -0
  81. package/dist/tenant/resolve.mjs +69 -0
  82. package/dist/tenant/types.d.mts +142 -0
  83. package/dist/testing/conformance.mjs +666 -17
  84. package/dist/testing/index.d.mts +2 -2
  85. package/dist/testing/types.d.mts +99 -2
  86. package/package.json +27 -1
  87. package/dist/cache/stable-stringify.d.mts +0 -15
@@ -0,0 +1,222 @@
1
+ import { DeleteResult, MinimalRepo, StandardRepo } from "../repository/types.mjs";
2
+ import { SchemaBuilderOptions } from "../schema/types.mjs";
3
+
4
+ //#region src/adapter/types.d.ts
5
+ /**
6
+ * Cross-kit repository contract.
7
+ *
8
+ * Defined as `MinimalRepo<TDoc> & Partial<StandardRepo<TDoc>>` — the
9
+ * 5-method floor every kit must implement, plus every other
10
+ * `StandardRepo` method (atomic CAS, batch ops, aggregation, soft-delete,
11
+ * transactions) as optional. Hosts feature-detect optional methods at
12
+ * call sites; kits declare only what they implement.
13
+ *
14
+ * **Why compound and not `StandardRepo` alone:** forcing every kit to
15
+ * implement the full surface would break kits with partial capabilities
16
+ * (sqlitekit has no aggregation, prismakit has no native atomic CAS the
17
+ * same way). Hosts use `typeof repo.method === 'function'` checks at
18
+ * construction.
19
+ *
20
+ * **Why compound and not `MinimalRepo` alone:** internal subsystems
21
+ * (audit, outbox, idempotency stores) need `StandardRepo` type info at
22
+ * call sites. `Partial<StandardRepo>` keeps the type-level backing
23
+ * without forcing every kit to implement everything.
24
+ */
25
+ type RepositoryLike<TDoc = unknown> = MinimalRepo<TDoc> & Partial<StandardRepo<TDoc>>;
26
+ /**
27
+ * Permissive structural input accepted at every adapter factory boundary.
28
+ *
29
+ * Wider than `RepositoryLike<TDoc>` on `getAll`'s `params`/`options` —
30
+ * uses method-shorthand syntax with `unknown` so kit-native repository
31
+ * classes plug in directly without `as RepositoryLike<TDoc>` casts on
32
+ * the host.
33
+ *
34
+ * **Why this exists.** repo-core 0.2 widened `MinimalRepo['getAll']`'s
35
+ * `params.filters` to a `Filter | Record<string, unknown>` IR union, but
36
+ * concrete kit `Repository` classes still type `filters` as the narrower
37
+ * `Record<string, unknown>`. Under `strictFunctionTypes` the kit's
38
+ * narrower function-property `getAll` is no longer assignable to the
39
+ * IR-aware one, which forced every host adapter glue file to write
40
+ * `repository as unknown as RepositoryLike<TDoc>`.
41
+ *
42
+ * Adapter factories accept this permissive shape, then call
43
+ * `asRepositoryLike()` once to widen for host internals (which still see
44
+ * the strict `RepositoryLike` view). The documented escape hatch lives
45
+ * in repo-core, not at every host call site.
46
+ */
47
+ interface AdapterRepositoryInput<TDoc = unknown> {
48
+ readonly idField?: string;
49
+ getAll(params?: unknown, options?: unknown): Promise<unknown>;
50
+ getById(id: string, options?: unknown): Promise<TDoc | null>;
51
+ create(data: Partial<TDoc>, options?: unknown): Promise<TDoc>;
52
+ update(id: string, data: Partial<TDoc>, options?: unknown): Promise<TDoc | null>;
53
+ delete(id: string, options?: unknown): Promise<DeleteResult | null>;
54
+ }
55
+ /**
56
+ * Generic OpenAPI-shaped schema bag emitted by `DataAdapter.generateSchemas`.
57
+ *
58
+ * Loose `unknown` slots so kits emit JSON Schema or kit-native shapes
59
+ * without type pressure. Hosts that want a specific shape (Fastify route
60
+ * schemas, Zod models, ...) narrow on consumption.
61
+ */
62
+ interface OpenApiSchemas {
63
+ /** Resource entity schema (the row shape). */
64
+ entity?: unknown;
65
+ /** Create-body schema (POST / PUT body). */
66
+ createBody?: unknown;
67
+ /** Update-body schema (PATCH body). */
68
+ updateBody?: unknown;
69
+ /** Path-params schema (`/:id`). */
70
+ params?: unknown;
71
+ /** List-query querystring schema (filtering / pagination / sort). */
72
+ listQuery?: unknown;
73
+ /**
74
+ * Response schema for OpenAPI documentation. Auto-derived from the
75
+ * entity / create-body shape if omitted.
76
+ */
77
+ response?: unknown;
78
+ [key: string]: unknown;
79
+ }
80
+ /**
81
+ * Context passed to `adapter.generateSchemas()` so adapters shape output
82
+ * to match host-level configuration. All fields optional — adapters that
83
+ * ignore this still work; the host applies its own normalization.
84
+ */
85
+ interface AdapterSchemaContext {
86
+ /** The `idField` configured on the resource. Defaults to `_id`. */
87
+ idField?: string;
88
+ /** Resource name (for error messages / logging). */
89
+ resourceName?: string;
90
+ }
91
+ /**
92
+ * Field-level metadata returned by `getSchemaMetadata()`. JSON-Schema-
93
+ * adjacent vocabulary kept driver-free so introspection tooling can
94
+ * consume any kit's output uniformly.
95
+ */
96
+ interface FieldMetadata {
97
+ type: 'string' | 'number' | 'boolean' | 'date' | 'object' | 'array' | 'objectId' | 'enum';
98
+ required?: boolean;
99
+ unique?: boolean;
100
+ default?: unknown;
101
+ enum?: Array<string | number>;
102
+ min?: number;
103
+ max?: number;
104
+ minLength?: number;
105
+ maxLength?: number;
106
+ pattern?: string;
107
+ description?: string;
108
+ ref?: string;
109
+ array?: boolean;
110
+ }
111
+ /**
112
+ * Relation metadata returned by `getSchemaMetadata()`.
113
+ */
114
+ interface RelationMetadata {
115
+ type: 'one-to-one' | 'one-to-many' | 'many-to-many';
116
+ target: string;
117
+ foreignKey?: string;
118
+ through?: string;
119
+ }
120
+ /**
121
+ * Schema metadata returned by `getSchemaMetadata()`. Shape-only — no kit
122
+ * types leak through.
123
+ */
124
+ interface SchemaMetadata {
125
+ name: string;
126
+ fields: Record<string, FieldMetadata>;
127
+ indexes?: Array<{
128
+ fields: string[];
129
+ unique?: boolean;
130
+ sparse?: boolean;
131
+ }>;
132
+ relations?: Record<string, RelationMetadata>;
133
+ }
134
+ /**
135
+ * Result of `adapter.validate()`. Structurally identical to repo-core's
136
+ * `schema/types.ts` `ValidationResult` for the message/violations shape,
137
+ * but uses `errors[]` (the OpenAPI/AJV convention) for adapter-time
138
+ * validation. Kept distinct so adapter validation and update-body
139
+ * validation can evolve independently.
140
+ */
141
+ interface AdapterValidationResult {
142
+ valid: boolean;
143
+ errors?: Array<{
144
+ field: string;
145
+ message: string;
146
+ code?: string;
147
+ }>;
148
+ }
149
+ /**
150
+ * Cross-framework data-adapter contract.
151
+ *
152
+ * A kit's `createXxxAdapter()` factory produces an instance of this
153
+ * interface. Frameworks (arc, custom hosts) consume the same shape; the
154
+ * kit never imports the framework.
155
+ */
156
+ interface DataAdapter<TDoc = unknown> {
157
+ /**
158
+ * Repository implementing CRUD operations. Any value that satisfies
159
+ * `RepositoryLike<TDoc>` — which includes `StandardRepo<TDoc>` (all
160
+ * methods implemented), `MinimalRepo<TDoc>` (5-method floor), or
161
+ * anything in between a kit declares. Hosts feature-detect optional
162
+ * methods at runtime.
163
+ */
164
+ repository: RepositoryLike<TDoc>;
165
+ /** Adapter identifier for introspection. */
166
+ readonly type: 'mongoose' | 'prisma' | 'drizzle' | 'typeorm' | 'custom';
167
+ /** Human-readable name. */
168
+ readonly name: string;
169
+ /**
170
+ * Generate OpenAPI-shaped schemas for CRUD operations. Each adapter
171
+ * produces schemas appropriate to its ORM/database (mongokit
172
+ * introspects Mongoose paths; sqlitekit introspects Drizzle columns).
173
+ *
174
+ * Options use repo-core's `SchemaBuilderOptions` floor — host-specific
175
+ * extensions (arc's `RouteSchemaOptions`) extend this base structurally.
176
+ *
177
+ * @param options - Schema generation options (field rules, populate hints).
178
+ * @param context - Resource-level context (`idField` for params shape,
179
+ * `name` for logs).
180
+ */
181
+ generateSchemas?(options?: SchemaBuilderOptions, context?: AdapterSchemaContext): OpenApiSchemas | Record<string, unknown> | null;
182
+ /** Extract schema metadata for OpenAPI / introspection. */
183
+ getSchemaMetadata?(): SchemaMetadata | null;
184
+ /** Validate data against schema before persistence. */
185
+ validate?(data: unknown): Promise<AdapterValidationResult> | AdapterValidationResult;
186
+ /** Health check for database connection. */
187
+ healthCheck?(): Promise<boolean>;
188
+ /**
189
+ * Custom filter matching for in-memory policy enforcement. Falls back
190
+ * to the host's built-in shallow matcher when omitted. Override for
191
+ * SQL adapters, non-Mongo operators, or kits that compile Filter IR.
192
+ */
193
+ matchesFilter?: (item: unknown, filters: Record<string, unknown>) => boolean;
194
+ /** Close / cleanup resources. */
195
+ close?(): Promise<void>;
196
+ /**
197
+ * Optional: does the underlying schema declare a path with this name?
198
+ *
199
+ * Used by hosts (e.g. arc's `defineResource()`) to infer absent tenant
200
+ * fields — without this hook, hosts who forget `tenantField: false` on
201
+ * cross-tenant tables get queries silently filtered to zero results.
202
+ * Adapters that can introspect their schema implement it; ones that
203
+ * can't omit it (the host falls back to its default behaviour).
204
+ *
205
+ * Implementation guidance:
206
+ * - Mongoose: `Boolean(this.model.schema.paths[name])`.
207
+ * - Drizzle / SQL kits: check column metadata.
208
+ *
209
+ * @returns `true` if the schema declares the path, `false` if not,
210
+ * `undefined` if the adapter can't determine it (treated as
211
+ * "unknown" — same as omitting the method).
212
+ */
213
+ hasFieldPath?(name: string): boolean | undefined;
214
+ }
215
+ /**
216
+ * Adapter factory signature. A kit's `createXxxAdapter(config)` matches
217
+ * this shape — config is kit-specific so adapters can accept their own
218
+ * options (e.g. `{ model, schemaGenerator, ... }`).
219
+ */
220
+ type AdapterFactory<TDoc = unknown> = (config: unknown) => DataAdapter<TDoc>;
221
+ //#endregion
222
+ export { AdapterFactory, AdapterRepositoryInput, AdapterSchemaContext, AdapterValidationResult, DataAdapter, FieldMetadata, OpenApiSchemas, RelationMetadata, RepositoryLike, SchemaMetadata };
@@ -0,0 +1,22 @@
1
+ import { AdapterRepositoryInput, RepositoryLike } from "./types.mjs";
2
+
3
+ //#region src/adapter/widen.d.ts
4
+ /**
5
+ * Widen a permissive `AdapterRepositoryInput<TDoc>` to the strict
6
+ * `RepositoryLike<TDoc>` view used by host internals.
7
+ *
8
+ * Single-source cast — kit adapters call this once at their factory
9
+ * boundary; host code (arc, future arc-next) consumes the strict view
10
+ * everywhere else.
11
+ */
12
+ declare function asRepositoryLike<TDoc = unknown>(input: AdapterRepositoryInput<TDoc>): RepositoryLike<TDoc>;
13
+ /**
14
+ * Runtime guard: does `value` look like a `RepositoryLike<TDoc>`?
15
+ *
16
+ * Checks for the five required methods (`getAll`, `getById`, `create`,
17
+ * `update`, `delete`) plus the optional `idField`. Used by adapter
18
+ * factories to validate input before wrapping.
19
+ */
20
+ declare function isRepository<TDoc = unknown>(value: unknown): value is RepositoryLike<TDoc>;
21
+ //#endregion
22
+ export { asRepositoryLike, isRepository };
@@ -0,0 +1,26 @@
1
+ //#region src/adapter/widen.ts
2
+ /**
3
+ * Widen a permissive `AdapterRepositoryInput<TDoc>` to the strict
4
+ * `RepositoryLike<TDoc>` view used by host internals.
5
+ *
6
+ * Single-source cast — kit adapters call this once at their factory
7
+ * boundary; host code (arc, future arc-next) consumes the strict view
8
+ * everywhere else.
9
+ */
10
+ function asRepositoryLike(input) {
11
+ return input;
12
+ }
13
+ /**
14
+ * Runtime guard: does `value` look like a `RepositoryLike<TDoc>`?
15
+ *
16
+ * Checks for the five required methods (`getAll`, `getById`, `create`,
17
+ * `update`, `delete`) plus the optional `idField`. Used by adapter
18
+ * factories to validate input before wrapping.
19
+ */
20
+ function isRepository(value) {
21
+ if (!value || typeof value !== "object") return false;
22
+ const v = value;
23
+ return typeof v["getAll"] === "function" && typeof v["getById"] === "function" && typeof v["create"] === "function" && typeof v["update"] === "function" && typeof v["delete"] === "function";
24
+ }
25
+ //#endregion
26
+ export { asRepositoryLike, isRepository };
@@ -0,0 +1,3 @@
1
+ import { DecodedCursor, decodeAggCursor, encodeAggCursor, isKeysetMode } from "./keyset.mjs";
2
+ import { normalizeGroupBy, validateMeasures } from "./normalize.mjs";
3
+ export { type DecodedCursor, decodeAggCursor, encodeAggCursor, isKeysetMode, normalizeGroupBy, validateMeasures };
@@ -0,0 +1,3 @@
1
+ import { decodeAggCursor, encodeAggCursor, isKeysetMode } from "./keyset.mjs";
2
+ import { normalizeGroupBy, validateMeasures } from "./normalize.mjs";
3
+ export { decodeAggCursor, encodeAggCursor, isKeysetMode, normalizeGroupBy, validateMeasures };
@@ -0,0 +1,57 @@
1
+ //#region src/aggregate/keyset.d.ts
2
+ /**
3
+ * Keyset (cursor) pagination helpers — the portable, kit-neutral half.
4
+ *
5
+ * Cursor encode/decode is identical across kits: serialize the sort-key
6
+ * tuple of the last row, base64url it, hand back. Every kit's
7
+ * `aggregatePaginate(req)` produced byte-identical encodings before
8
+ * this module landed — the duplication served no purpose.
9
+ *
10
+ * What this module does NOT cover: building the kit-specific predicate
11
+ * that selects rows AFTER the cursor. Mongo emits a `$match` JSON
12
+ * stage; SQL emits a `HAVING (col1, col2) > (?, ?)` Drizzle SQL
13
+ * fragment. Those compilers stay in each kit because they speak the
14
+ * driver's query language. They consume `DecodedCursor` from here.
15
+ *
16
+ * **Cross-kit cursor compatibility is not promised.** The encoded
17
+ * cursor depends on which keys the kit's sort spec ends up using — a
18
+ * Mongo cursor with `_id` won't round-trip on a SQL kit using `id`.
19
+ * Consumers MUST round-trip cursors verbatim against the same backend
20
+ * that produced them.
21
+ */
22
+ /**
23
+ * Decoded cursor — sort-key → value tuples from the last row of the
24
+ * prior page. Values are JSON-serialisable scalars (numbers, strings,
25
+ * booleans, ISO date strings). `undefined` and `null` collapse to
26
+ * `null` on encode so the round-trip is stable.
27
+ */
28
+ type DecodedCursor = Record<string, unknown>;
29
+ /**
30
+ * Encode a cursor from the last row of a page given the sort spec.
31
+ *
32
+ * Only sort keys are extracted — the cursor MUST be small (it travels
33
+ * over the URL on every "next page" request). Carrying the full row
34
+ * would inflate cursors with measure values and group keys that
35
+ * aren't load-bearing for pagination.
36
+ */
37
+ declare function encodeAggCursor(row: Record<string, unknown>, sort: Record<string, 1 | -1>): string;
38
+ /**
39
+ * Decode a cursor previously produced by `encodeAggCursor`. Throws on
40
+ * any malformed cursor — callers should treat the throw as "client
41
+ * sent garbage" and surface a 400-class error rather than masking it.
42
+ *
43
+ * The `kitName` prefix on the error message keeps stack-trace context
44
+ * legible (`'mongokit/aggregate: ...'` vs `'sqlitekit/aggregate: ...'`).
45
+ */
46
+ declare function decodeAggCursor(cursor: string, kitName: string): DecodedCursor;
47
+ /**
48
+ * Pick the keyset mode flag from the request shape. `pagination:
49
+ * 'keyset'` is the explicit form; setting `after` implies keyset
50
+ * (handing back a cursor token in offset mode would be a wiring bug).
51
+ */
52
+ declare function isKeysetMode(req: {
53
+ pagination?: string;
54
+ after?: string;
55
+ }): boolean;
56
+ //#endregion
57
+ export { DecodedCursor, decodeAggCursor, encodeAggCursor, isKeysetMode };
@@ -0,0 +1,45 @@
1
+ //#region src/aggregate/keyset.ts
2
+ /**
3
+ * Encode a cursor from the last row of a page given the sort spec.
4
+ *
5
+ * Only sort keys are extracted — the cursor MUST be small (it travels
6
+ * over the URL on every "next page" request). Carrying the full row
7
+ * would inflate cursors with measure values and group keys that
8
+ * aren't load-bearing for pagination.
9
+ */
10
+ function encodeAggCursor(row, sort) {
11
+ const tuple = {};
12
+ for (const key of Object.keys(sort)) tuple[key] = row[key];
13
+ return Buffer.from(JSON.stringify(tuple), "utf8").toString("base64url");
14
+ }
15
+ /**
16
+ * Decode a cursor previously produced by `encodeAggCursor`. Throws on
17
+ * any malformed cursor — callers should treat the throw as "client
18
+ * sent garbage" and surface a 400-class error rather than masking it.
19
+ *
20
+ * The `kitName` prefix on the error message keeps stack-trace context
21
+ * legible (`'mongokit/aggregate: ...'` vs `'sqlitekit/aggregate: ...'`).
22
+ */
23
+ function decodeAggCursor(cursor, kitName) {
24
+ let parsed;
25
+ try {
26
+ const json = Buffer.from(cursor, "base64url").toString("utf8");
27
+ parsed = JSON.parse(json);
28
+ } catch (cause) {
29
+ throw new Error(`${kitName}/aggregate: malformed keyset cursor — base64url+JSON decode failed (${cause.message})`);
30
+ }
31
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`${kitName}/aggregate: malformed keyset cursor — expected an object payload`);
32
+ return parsed;
33
+ }
34
+ /**
35
+ * Pick the keyset mode flag from the request shape. `pagination:
36
+ * 'keyset'` is the explicit form; setting `after` implies keyset
37
+ * (handing back a cursor token in offset mode would be a wiring bug).
38
+ */
39
+ function isKeysetMode(req) {
40
+ if (req.pagination === "keyset") return true;
41
+ if (typeof req.after === "string" && req.after.length > 0) return true;
42
+ return false;
43
+ }
44
+ //#endregion
45
+ export { decodeAggCursor, encodeAggCursor, isKeysetMode };
@@ -0,0 +1,24 @@
1
+ import { AggRequest } from "../repository/types.mjs";
2
+
3
+ //#region src/aggregate/normalize.d.ts
4
+ /**
5
+ * Normalize `AggRequest['groupBy']` into a readonly string array.
6
+ * Returns `[]` for scalar aggregation (no groupBy). Downstream
7
+ * compilers treat `[]` uniformly — Mongo emits `$group: { _id: null }`,
8
+ * SQL emits a single SELECT without a GROUP BY clause.
9
+ */
10
+ declare function normalizeGroupBy(groupBy: AggRequest['groupBy']): readonly string[];
11
+ /**
12
+ * Fail loud on an empty measures bag — there's nothing to compute and
13
+ * the caller's code path is almost certainly a wiring bug (conditional
14
+ * collapsed, key renamed, etc.). Silently returning `{ rows: [] }`
15
+ * would mask it.
16
+ *
17
+ * The `kitName` prefix (`'mongokit'`, `'sqlitekit'`, ...) on the error
18
+ * message keeps stack-trace context legible — when a developer sees
19
+ * the throw they know which kit's compiler raised it without having
20
+ * to walk the trace.
21
+ */
22
+ declare function validateMeasures(measures: AggRequest['measures'], kitName: string): void;
23
+ //#endregion
24
+ export { normalizeGroupBy, validateMeasures };
@@ -0,0 +1,28 @@
1
+ //#region src/aggregate/normalize.ts
2
+ /**
3
+ * Normalize `AggRequest['groupBy']` into a readonly string array.
4
+ * Returns `[]` for scalar aggregation (no groupBy). Downstream
5
+ * compilers treat `[]` uniformly — Mongo emits `$group: { _id: null }`,
6
+ * SQL emits a single SELECT without a GROUP BY clause.
7
+ */
8
+ function normalizeGroupBy(groupBy) {
9
+ if (!groupBy) return [];
10
+ if (typeof groupBy === "string") return [groupBy];
11
+ return groupBy;
12
+ }
13
+ /**
14
+ * Fail loud on an empty measures bag — there's nothing to compute and
15
+ * the caller's code path is almost certainly a wiring bug (conditional
16
+ * collapsed, key renamed, etc.). Silently returning `{ rows: [] }`
17
+ * would mask it.
18
+ *
19
+ * The `kitName` prefix (`'mongokit'`, `'sqlitekit'`, ...) on the error
20
+ * message keeps stack-trace context legible — when a developer sees
21
+ * the throw they know which kit's compiler raised it without having
22
+ * to walk the trace.
23
+ */
24
+ function validateMeasures(measures, kitName) {
25
+ if (!measures || Object.keys(measures).length === 0) throw new Error(`${kitName}/aggregate: AggRequest requires at least one measure — empty measures bag is a wiring bug`);
26
+ }
27
+ //#endregion
28
+ export { normalizeGroupBy, validateMeasures };
@@ -0,0 +1,110 @@
1
+ //#region src/better-auth/index.d.ts
2
+ /**
3
+ * Better Auth — kit-agnostic registry of which collections each plugin owns.
4
+ *
5
+ * This module ships **zero DB code**. It's the source of truth that every
6
+ * kit's `better-auth` overlay subpath consumes (`@classytic/mongokit/better-auth`,
7
+ * `@classytic/sqlitekit/better-auth`, future `@classytic/prismakit/better-auth`)
8
+ * so the per-kit overlays don't each maintain their own copy of the
9
+ * plugin → collection list.
10
+ *
11
+ * Why repo-core: kits depend on repo-core; arc/hosts consume kits. Putting the
12
+ * registry one level below the kit layer keeps it accessible to every kit
13
+ * without forcing kits to peer-dep each other or to peer-dep arc.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { resolveBetterAuthCollections } from '@classytic/repo-core/better-auth';
18
+ *
19
+ * const names = resolveBetterAuthCollections({
20
+ * plugins: ['organization'],
21
+ * extraCollections: ['passkey'],
22
+ * });
23
+ * // → ['user', 'session', 'account', 'verification', 'organization', 'member', 'invitation', 'passkey']
24
+ * ```
25
+ */
26
+ /**
27
+ * Plugin keys that map to Better Auth collection sets.
28
+ *
29
+ * Only plugins that ship inside the **core `better-auth` package** are listed
30
+ * here. Plugins distributed as separate `@better-auth/*` packages
31
+ * (api-key, passkey, sso, oauth-provider, etc.) evolve independently and
32
+ * should be handled via `extraCollections` — see `resolveBetterAuthCollections`.
33
+ *
34
+ * Plugins that only add *fields* to existing tables (admin, username,
35
+ * phoneNumber, magicLink, emailOtp, anonymous, bearer, multiSession, siwe,
36
+ * lastLoginMethod, genericOAuth, etc.) don't need an entry — kit overlays
37
+ * register schemas with `strict: false` (Mongoose) or pass-through column
38
+ * mappings (SQL), so extra fields round-trip automatically.
39
+ *
40
+ * - `core` — always included; covers `user`, `session`, `account`, `verification`.
41
+ * - `organization` — adds `organization`, `member`, `invitation`.
42
+ * - `organization-teams` — adds `team`, `teamMember` (only when `teams.enabled`).
43
+ * - `twoFactor` — adds `twoFactor`.
44
+ * - `jwt` — adds `jwks`.
45
+ * - `oidcProvider` — adds `oauthApplication`, `oauthAccessToken`, `oauthConsent`.
46
+ * - `oauthProvider` — alias of `oidcProvider` (same schema).
47
+ * - `mcp` — MCP plugin reuses the oidcProvider schema (BA docs are explicit).
48
+ * - `deviceAuthorization` — adds `deviceCode` (RFC 8628 device authorization).
49
+ */
50
+ type BetterAuthPluginKey = 'core' | 'organization' | 'organization-teams' | 'twoFactor' | 'jwt' | 'oidcProvider' | 'oauthProvider' | 'mcp' | 'deviceAuthorization';
51
+ /**
52
+ * Canonical collection lists per plugin. `core` is always implied by
53
+ * `resolveBetterAuthCollections` — callers don't need to pass it.
54
+ *
55
+ * Naming follows BA's mongo adapter (`usePlural: false`) convention. Hosts
56
+ * that opted into `usePlural: true` should pass that flag to the kit overlay,
57
+ * which appends `s` to each name (`user` → `users`).
58
+ */
59
+ declare const BA_COLLECTIONS_BY_PLUGIN: Record<BetterAuthPluginKey, readonly string[]>;
60
+ /**
61
+ * Naive English pluralization that matches Better Auth's `usePlural` behavior:
62
+ * BA's mongo adapter just appends `s` (it doesn't handle irregular nouns —
63
+ * none of its collection names are irregular). Kit overlays mirror this.
64
+ */
65
+ declare function pluralizeBetterAuthCollection(name: string): string;
66
+ interface ResolveBetterAuthCollectionsOptions {
67
+ /**
68
+ * Which plugin collection sets to include. `core` is always implied —
69
+ * you don't need to pass it.
70
+ *
71
+ * @default []
72
+ */
73
+ plugins?: BetterAuthPluginKey[];
74
+ /**
75
+ * Additional collection names beyond the built-in plugin set.
76
+ *
77
+ * Use for plugins that ship as separate `@better-auth/*` packages — their
78
+ * collection names live in their own packages and are intentionally not
79
+ * hardcoded here so they can evolve independently.
80
+ *
81
+ * Known names for official separate-package plugins:
82
+ * - `@better-auth/passkey` → `'passkey'`
83
+ * - `@better-auth/sso` → `'ssoProvider'`
84
+ * - `@better-auth/oauth-provider` → already covered by `plugins: ['oauthProvider']`
85
+ *
86
+ * @default []
87
+ */
88
+ extraCollections?: string[];
89
+ /**
90
+ * When BA's adapter was configured with `usePlural: true`, every name is
91
+ * pluralized (`user` → `users`). Must match what you passed to BA's adapter.
92
+ *
93
+ * @default false
94
+ */
95
+ usePlural?: boolean;
96
+ /**
97
+ * Per-collection name override. Applies AFTER pluralization. Use when
98
+ * you've passed `user: { modelName: 'profile' }` (or similar) to
99
+ * `betterAuth()` — pass the same map here so downstream consumers
100
+ * (kit overlays, populate resolution) line up.
101
+ */
102
+ modelOverrides?: Partial<Record<string, string>>;
103
+ }
104
+ /**
105
+ * Resolve a plugin set + extras to a deduplicated, ordered list of
106
+ * collection names. `core` is always included.
107
+ */
108
+ declare function resolveBetterAuthCollections(options?: ResolveBetterAuthCollectionsOptions): string[];
109
+ //#endregion
110
+ export { BA_COLLECTIONS_BY_PLUGIN, BetterAuthPluginKey, ResolveBetterAuthCollectionsOptions, pluralizeBetterAuthCollection, resolveBetterAuthCollections };
@@ -0,0 +1,71 @@
1
+ //#region src/better-auth/index.ts
2
+ /**
3
+ * Canonical collection lists per plugin. `core` is always implied by
4
+ * `resolveBetterAuthCollections` — callers don't need to pass it.
5
+ *
6
+ * Naming follows BA's mongo adapter (`usePlural: false`) convention. Hosts
7
+ * that opted into `usePlural: true` should pass that flag to the kit overlay,
8
+ * which appends `s` to each name (`user` → `users`).
9
+ */
10
+ const BA_COLLECTIONS_BY_PLUGIN = {
11
+ core: [
12
+ "user",
13
+ "session",
14
+ "account",
15
+ "verification"
16
+ ],
17
+ organization: [
18
+ "organization",
19
+ "member",
20
+ "invitation"
21
+ ],
22
+ "organization-teams": ["team", "teamMember"],
23
+ twoFactor: ["twoFactor"],
24
+ jwt: ["jwks"],
25
+ oidcProvider: [
26
+ "oauthApplication",
27
+ "oauthAccessToken",
28
+ "oauthConsent"
29
+ ],
30
+ oauthProvider: [
31
+ "oauthApplication",
32
+ "oauthAccessToken",
33
+ "oauthConsent"
34
+ ],
35
+ mcp: [
36
+ "oauthApplication",
37
+ "oauthAccessToken",
38
+ "oauthConsent"
39
+ ],
40
+ deviceAuthorization: ["deviceCode"]
41
+ };
42
+ /**
43
+ * Naive English pluralization that matches Better Auth's `usePlural` behavior:
44
+ * BA's mongo adapter just appends `s` (it doesn't handle irregular nouns —
45
+ * none of its collection names are irregular). Kit overlays mirror this.
46
+ */
47
+ function pluralizeBetterAuthCollection(name) {
48
+ return name.endsWith("s") ? name : `${name}s`;
49
+ }
50
+ /**
51
+ * Resolve a plugin set + extras to a deduplicated, ordered list of
52
+ * collection names. `core` is always included.
53
+ */
54
+ function resolveBetterAuthCollections(options = {}) {
55
+ const { plugins = [], extraCollections = [], usePlural = false, modelOverrides = {} } = options;
56
+ const pluginSet = new Set(["core", ...plugins]);
57
+ const collected = [];
58
+ for (const key of pluginSet) for (const name of BA_COLLECTIONS_BY_PLUGIN[key]) collected.push(name);
59
+ for (const name of extraCollections) collected.push(name);
60
+ const seen = /* @__PURE__ */ new Set();
61
+ const unique = [];
62
+ for (const canonical of collected) {
63
+ if (seen.has(canonical)) continue;
64
+ seen.add(canonical);
65
+ const finalName = modelOverrides[canonical] ?? (usePlural ? pluralizeBetterAuthCollection(canonical) : canonical);
66
+ unique.push(finalName);
67
+ }
68
+ return unique;
69
+ }
70
+ //#endregion
71
+ export { BA_COLLECTIONS_BY_PLUGIN, pluralizeBetterAuthCollection, resolveBetterAuthCollections };