@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.
- package/CHANGELOG.md +363 -0
- package/README.md +28 -7
- package/dist/adapter/index.d.mts +3 -0
- package/dist/adapter/index.mjs +2 -0
- package/dist/adapter/types.d.mts +222 -0
- package/dist/adapter/widen.d.mts +22 -0
- package/dist/adapter/widen.mjs +26 -0
- package/dist/aggregate/index.d.mts +3 -0
- package/dist/aggregate/index.mjs +3 -0
- package/dist/aggregate/keyset.d.mts +57 -0
- package/dist/aggregate/keyset.mjs +45 -0
- package/dist/aggregate/normalize.d.mts +24 -0
- package/dist/aggregate/normalize.mjs +28 -0
- package/dist/better-auth/index.d.mts +110 -0
- package/dist/better-auth/index.mjs +71 -0
- package/dist/cache/engine.d.mts +127 -0
- package/dist/cache/engine.mjs +235 -0
- package/dist/cache/envelope.mjs +32 -0
- package/dist/cache/index.d.mts +7 -2
- package/dist/cache/index.mjs +6 -2
- package/dist/cache/keys.mjs +131 -0
- package/dist/cache/memory-adapter.mjs +41 -7
- package/dist/cache/options.d.mts +112 -0
- package/dist/cache/options.mjs +25 -0
- package/dist/cache/plugin/context.d.mts +18 -0
- package/dist/cache/plugin/context.mjs +121 -0
- package/dist/cache/plugin/index.d.mts +86 -0
- package/dist/cache/plugin/index.mjs +78 -0
- package/dist/cache/plugin/invalidation-hooks.mjs +35 -0
- package/dist/cache/plugin/read-hooks.mjs +96 -0
- package/dist/cache/plugin/swr.mjs +20 -0
- package/dist/cache/runtime.d.mts +43 -0
- package/dist/cache/runtime.mjs +14 -0
- package/dist/cache/tag-index.mjs +84 -0
- package/dist/cache/timeout-adapter.d.mts +30 -0
- package/dist/cache/timeout-adapter.mjs +58 -0
- package/dist/cache/types.d.mts +45 -0
- package/dist/cache/version-store.mjs +57 -0
- package/dist/errors/contract.d.mts +37 -0
- package/dist/errors/contract.mjs +75 -0
- package/dist/errors/index.d.mts +4 -2
- package/dist/errors/index.mjs +4 -1
- package/dist/errors/schema.d.mts +101 -0
- package/dist/errors/schema.mjs +78 -0
- package/dist/errors/types.d.mts +113 -8
- package/dist/errors/types.mjs +29 -0
- package/dist/filter/match.mjs +38 -2
- package/dist/pagination/canonical.d.mts +35 -0
- package/dist/pagination/canonical.mjs +26 -0
- package/dist/pagination/cursor.mjs +4 -1
- package/dist/pagination/index.d.mts +3 -2
- package/dist/pagination/index.mjs +2 -1
- package/dist/pagination/types.d.mts +57 -3
- package/dist/plugins/index.d.mts +2 -0
- package/dist/plugins/index.mjs +2 -0
- package/dist/plugins/tenant-helpers.d.mts +63 -0
- package/dist/plugins/tenant-helpers.mjs +84 -0
- package/dist/query-parser/index.d.mts +2 -1
- package/dist/query-parser/index.mjs +2 -1
- package/dist/query-parser/parse-url.mjs +13 -11
- package/dist/query-parser/reserved.d.mts +43 -0
- package/dist/query-parser/reserved.mjs +56 -0
- package/dist/repository/agg-output.d.mts +63 -0
- package/dist/repository/agg-output.mjs +89 -0
- package/dist/repository/base.mjs +21 -0
- package/dist/repository/index.d.mts +4 -2
- package/dist/repository/index.mjs +3 -1
- package/dist/repository/options.d.mts +62 -0
- package/dist/repository/options.mjs +57 -0
- package/dist/repository/types.d.mts +935 -48
- package/dist/schema/field-rules.d.mts +60 -9
- package/dist/schema/field-rules.mjs +121 -10
- package/dist/schema/generator.d.mts +72 -0
- package/dist/schema/generator.mjs +16 -0
- package/dist/schema/index.d.mts +3 -2
- package/dist/schema/index.mjs +3 -2
- package/dist/schema/types.d.mts +77 -3
- package/dist/tenant/index.d.mts +3 -0
- package/dist/tenant/index.mjs +2 -0
- package/dist/tenant/resolve.d.mts +27 -0
- package/dist/tenant/resolve.mjs +69 -0
- package/dist/tenant/types.d.mts +142 -0
- package/dist/testing/conformance.mjs +666 -17
- package/dist/testing/index.d.mts +2 -2
- package/dist/testing/types.d.mts +99 -2
- package/package.json +27 -1
- 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,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 };
|