@atscript/utils-db 0.1.28

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.
@@ -0,0 +1,488 @@
1
+ import { TAtscriptAnnotatedType, TValidatorPlugin, TMetadataMap, TAtscriptDataType, Validator, TAtscriptTypeObject, TValidatorOptions, TAtscriptTypeArray } from '@atscript/typescript/utils';
2
+
3
+ /**
4
+ * Comparison and set operators for a single field value.
5
+ * Used inside filter objects: `{ age: { $gt: 18, $lt: 65 } }`.
6
+ */
7
+ type TFilterOperators<V = unknown> = {
8
+ $gt?: V;
9
+ $gte?: V;
10
+ $lt?: V;
11
+ $lte?: V;
12
+ $ne?: V | null;
13
+ $in?: V[];
14
+ $nin?: V[];
15
+ $exists?: boolean;
16
+ $regex?: string;
17
+ };
18
+ /**
19
+ * Typed filter for database queries. Root-level keys from `T` get
20
+ * autocompletion and value type checking. Arbitrary string keys
21
+ * (e.g. dot-notation paths like `"address.city"`) are allowed via
22
+ * index signature fallback.
23
+ *
24
+ * When used without a type parameter, behaves as `Record<string, unknown>`.
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * // Typed — autocompletion + value checking
29
+ * const filter: TDbFilter<User> = { name: 'John', age: { $gt: 18 } }
30
+ *
31
+ * // Dot-notation — accepted via string fallback
32
+ * const filter2: TDbFilter<User> = { 'address.city': 'NYC' }
33
+ * ```
34
+ */
35
+ type TDbFilter<T = Record<string, unknown>> = {
36
+ [K in keyof T & string]?: T[K] | TFilterOperators<T[K]> | null;
37
+ } & Record<string, unknown> & {
38
+ $and?: Array<TDbFilter<T>>;
39
+ $or?: Array<TDbFilter<T>>;
40
+ $not?: TDbFilter<T>;
41
+ };
42
+ /**
43
+ * Projection for selecting/excluding fields.
44
+ * Accepts either an object `{ name: 1, email: 1 }` or an array `['name', 'email']`.
45
+ * Root-level keys get autocompletion, arbitrary strings allowed for dot-notation.
46
+ */
47
+ type TDbProjection<T = Record<string, unknown>> = (Partial<Record<keyof T & string, 0 | 1>> & Record<string, 0 | 1>) | (keyof T & string | string)[];
48
+ /**
49
+ * Options for find operations: sort, pagination, projection.
50
+ * Root-level keys get autocompletion, arbitrary strings allowed for dot-notation.
51
+ */
52
+ interface TDbFindOptions<T = Record<string, unknown>> {
53
+ sort?: Partial<Record<keyof T & string, 1 | -1>> & Record<string, 1 | -1>;
54
+ skip?: number;
55
+ limit?: number;
56
+ projection?: TDbProjection<T>;
57
+ }
58
+ interface TDbInsertResult {
59
+ insertedId: unknown;
60
+ }
61
+ interface TDbInsertManyResult {
62
+ insertedCount: number;
63
+ insertedIds: unknown[];
64
+ }
65
+ interface TDbUpdateResult {
66
+ matchedCount: number;
67
+ modifiedCount: number;
68
+ }
69
+ interface TDbDeleteResult {
70
+ deletedCount: number;
71
+ }
72
+ interface TDbIndexField {
73
+ name: string;
74
+ sort: 'asc' | 'desc';
75
+ }
76
+ interface TDbIndex {
77
+ /** Unique key used for identity/diffing (e.g., "atscript__plain__email") */
78
+ key: string;
79
+ /** Human-readable index name. */
80
+ name: string;
81
+ /** Index type. */
82
+ type: 'plain' | 'unique' | 'fulltext';
83
+ /** Ordered list of fields in the index. */
84
+ fields: TDbIndexField[];
85
+ }
86
+ type TDbDefaultValue = {
87
+ kind: 'value';
88
+ value: string;
89
+ } | {
90
+ kind: 'fn';
91
+ fn: 'increment' | 'uuid' | 'now';
92
+ };
93
+ interface TIdDescriptor {
94
+ /** Field names that form the primary key. */
95
+ fields: string[];
96
+ /** Whether this is a composite key (multiple fields). */
97
+ isComposite: boolean;
98
+ }
99
+ interface TDbFieldMeta {
100
+ /** The dot-notation path to this field (logical name). */
101
+ path: string;
102
+ /** The annotated type for this field. */
103
+ type: TAtscriptAnnotatedType;
104
+ /** Physical column/field name (from @db.column, or same as path). */
105
+ physicalName: string;
106
+ /** Resolved design type: 'string', 'number', 'boolean', 'object', etc. */
107
+ designType: string;
108
+ /** Whether the field is optional. */
109
+ optional: boolean;
110
+ /** Whether this field is part of the primary key (@meta.id). */
111
+ isPrimaryKey: boolean;
112
+ /** Whether this field is excluded from the DB (@db.ignore). */
113
+ ignored: boolean;
114
+ /** Default value from @db.default.* */
115
+ defaultValue?: TDbDefaultValue;
116
+ }
117
+
118
+ /**
119
+ * Abstract base class for database adapters.
120
+ *
121
+ * Adapter instances are 1:1 with table instances. When an {@link AtscriptDbTable}
122
+ * is created with an adapter, it calls {@link registerTable} to establish a
123
+ * bidirectional relationship:
124
+ *
125
+ * ```
126
+ * AtscriptDbTable ──delegates CRUD──▶ BaseDbAdapter
127
+ * ◀──reads metadata── (via this._table)
128
+ * ```
129
+ *
130
+ * Adapter authors can access all computed metadata through `this._table`:
131
+ * - `this._table.tableName` — resolved table/collection name
132
+ * - `this._table.flatMap` — all fields as dot-notation paths
133
+ * - `this._table.indexes` — computed index definitions
134
+ * - `this._table.primaryKeys` — primary key field names
135
+ * - `this._table.columnMap` — logical → physical column mappings
136
+ * - `this._table.defaults` — default value configurations
137
+ * - `this._table.ignoredFields` — fields excluded from DB
138
+ * - `this._table.uniqueProps` — single-field unique index properties
139
+ */
140
+ declare abstract class BaseDbAdapter {
141
+ protected _table: AtscriptDbTable;
142
+ /**
143
+ * Called by {@link AtscriptDbTable} constructor. Gives the adapter access to
144
+ * the table's computed metadata for internal use in query rendering, index
145
+ * sync, etc.
146
+ */
147
+ registerTable(table: AtscriptDbTable): void;
148
+ /**
149
+ * Returns additional validator plugins for this adapter.
150
+ * These are merged with the built-in Atscript validators.
151
+ *
152
+ * Example: MongoDB adapter returns ObjectId validation plugin.
153
+ */
154
+ getValidatorPlugins(): TValidatorPlugin[];
155
+ /**
156
+ * Transforms an ID value for the database.
157
+ * Override to convert string → ObjectId, parse numeric IDs, etc.
158
+ *
159
+ * @param id - The raw ID value.
160
+ * @param fieldType - The annotated type of the ID field.
161
+ * @returns The transformed ID value.
162
+ */
163
+ prepareId(id: unknown, fieldType: TAtscriptAnnotatedType): unknown;
164
+ /**
165
+ * Whether this adapter supports native patch operations.
166
+ * When `true`, {@link AtscriptDbTable} delegates patch payloads to
167
+ * {@link nativePatch} instead of using the generic decomposition.
168
+ */
169
+ supportsNativePatch(): boolean;
170
+ /**
171
+ * Applies a patch payload using native database operations.
172
+ * Only called when {@link supportsNativePatch} returns `true`.
173
+ *
174
+ * @param filter - Filter identifying the record to patch.
175
+ * @param patch - The patch payload with array operations.
176
+ * @returns Update result.
177
+ */
178
+ nativePatch(filter: TDbFilter, patch: unknown): Promise<TDbUpdateResult>;
179
+ /**
180
+ * Called before field flattening begins.
181
+ * Use to extract table-level adapter-specific annotations.
182
+ *
183
+ * Example: MongoDB adapter extracts `@db.mongo.search.dynamic`.
184
+ */
185
+ onBeforeFlatten?(type: TAtscriptAnnotatedType): void;
186
+ /**
187
+ * Called for each field during flattening.
188
+ * Use to extract field-level adapter-specific annotations.
189
+ *
190
+ * Example: MongoDB adapter extracts `@db.mongo.index.text`, `@db.mongo.search.vector`.
191
+ */
192
+ onFieldScanned?(field: string, type: TAtscriptAnnotatedType, metadata: TMetadataMap<AtscriptMetadata>): void;
193
+ /**
194
+ * Called after all fields are scanned.
195
+ * Use to finalize adapter-specific computed state.
196
+ * Access table metadata via `this._table`.
197
+ */
198
+ onAfterFlatten?(): void;
199
+ /**
200
+ * Returns an adapter-specific table name.
201
+ * For example, MongoDB reads from `@db.mongo.collection`.
202
+ * Return `undefined` to fall back to `@db.table` or the interface name.
203
+ */
204
+ getAdapterTableName?(type: TAtscriptAnnotatedType): string | undefined;
205
+ /**
206
+ * Returns the metadata tag used to mark top-level arrays during flattening.
207
+ * Default: `'db.__topLevelArray'`
208
+ *
209
+ * Override to use adapter-specific tags (e.g., `'db.mongo.__topLevelArray'`).
210
+ */
211
+ getTopLevelArrayTag?(): string;
212
+ /**
213
+ * Resolves the full table name, optionally including the schema prefix.
214
+ * Override for databases that don't support schemas (e.g., SQLite).
215
+ *
216
+ * @param includeSchema - Whether to prepend `schema.` prefix (default: true).
217
+ */
218
+ resolveTableName(includeSchema?: boolean): string;
219
+ /**
220
+ * Template method for index synchronization.
221
+ * Implements the diff algorithm (list → compare → create/drop).
222
+ * Adapters provide the three DB-specific primitives.
223
+ *
224
+ * @example
225
+ * ```typescript
226
+ * async syncIndexes() {
227
+ * await this.syncIndexesWithDiff({
228
+ * listExisting: async () => this.driver.all('PRAGMA index_list(...)'),
229
+ * createIndex: async (index) => this.driver.exec('CREATE INDEX ...'),
230
+ * dropIndex: async (name) => this.driver.exec('DROP INDEX ...'),
231
+ * shouldSkipType: (type) => type === 'fulltext',
232
+ * })
233
+ * }
234
+ * ```
235
+ */
236
+ protected syncIndexesWithDiff(opts: {
237
+ listExisting(): Promise<Array<{
238
+ name: string;
239
+ }>>;
240
+ createIndex(index: TDbIndex): Promise<void>;
241
+ dropIndex(name: string): Promise<void>;
242
+ prefix?: string;
243
+ shouldSkipType?(type: TDbIndex['type']): boolean;
244
+ }): Promise<void>;
245
+ abstract insertOne(data: Record<string, unknown>): Promise<TDbInsertResult>;
246
+ abstract insertMany(data: Array<Record<string, unknown>>): Promise<TDbInsertManyResult>;
247
+ abstract replaceOne(filter: TDbFilter, data: Record<string, unknown>): Promise<TDbUpdateResult>;
248
+ abstract updateOne(filter: TDbFilter, data: Record<string, unknown>): Promise<TDbUpdateResult>;
249
+ abstract deleteOne(filter: TDbFilter): Promise<TDbDeleteResult>;
250
+ abstract findOne(filter: TDbFilter, options?: TDbFindOptions): Promise<Record<string, unknown> | null>;
251
+ abstract findMany(filter: TDbFilter, options?: TDbFindOptions): Promise<Array<Record<string, unknown>>>;
252
+ abstract count(filter: TDbFilter): Promise<number>;
253
+ abstract updateMany(filter: TDbFilter, data: Record<string, unknown>): Promise<TDbUpdateResult>;
254
+ abstract replaceMany(filter: TDbFilter, data: Record<string, unknown>): Promise<TDbUpdateResult>;
255
+ abstract deleteMany(filter: TDbFilter): Promise<TDbDeleteResult>;
256
+ /**
257
+ * Synchronizes indexes between the Atscript definitions and the database.
258
+ * Uses `this._table.indexes` for the full index definitions.
259
+ */
260
+ abstract syncIndexes(): Promise<void>;
261
+ /**
262
+ * Ensures the table exists in the database, creating it if needed.
263
+ * Uses `this._table.tableName`, `this._table.schema`, etc.
264
+ */
265
+ abstract ensureTable(): Promise<void>;
266
+ }
267
+
268
+ interface TGenericLogger {
269
+ error(...messages: any[]): void;
270
+ warn(...messages: any[]): void;
271
+ log(...messages: any[]): void;
272
+ info(...messages: any[]): void;
273
+ debug(...messages: any[]): void;
274
+ }
275
+ declare const NoopLogger: TGenericLogger;
276
+
277
+ /**
278
+ * Resolves the design type from an annotated type.
279
+ * Encapsulates the `kind === ''` check and fallback logic that
280
+ * otherwise trips up every adapter author.
281
+ */
282
+ declare function resolveDesignType(fieldType: TAtscriptAnnotatedType): string;
283
+ /**
284
+ * Generic database table abstraction driven by Atscript `@db.*` annotations.
285
+ *
286
+ * Accepts an annotated type marked with `@db.table` and a {@link BaseDbAdapter}
287
+ * instance. Pre-computes indexes, column mappings, defaults, and primary keys
288
+ * from annotations, then delegates actual database operations to the adapter.
289
+ *
290
+ * This class is **concrete** (not abstract) — extend it for cross-cutting
291
+ * concerns like field-level permissions, audit logging, etc. Extensions
292
+ * work with any adapter.
293
+ *
294
+ * ```typescript
295
+ * const adapter = new MongoAdapter(db)
296
+ * const users = new AtscriptDbTable(UsersType, adapter)
297
+ * await users.insertOne({ name: 'John', email: 'john@example.com' })
298
+ * ```
299
+ *
300
+ * @typeParam T - The Atscript annotated type for this table.
301
+ * @typeParam DataType - The inferred data shape from the annotated type.
302
+ */
303
+ declare class AtscriptDbTable<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType, DataType = TAtscriptDataType<T>> {
304
+ protected readonly _type: T;
305
+ protected readonly adapter: BaseDbAdapter;
306
+ protected readonly logger: TGenericLogger;
307
+ /** Resolved table/collection name. */
308
+ readonly tableName: string;
309
+ /** Database schema/namespace from `@db.schema` (if set). */
310
+ readonly schema: string | undefined;
311
+ protected _flatMap?: Map<string, TAtscriptAnnotatedType>;
312
+ protected _fieldDescriptors?: TDbFieldMeta[];
313
+ protected _indexes: Map<string, TDbIndex>;
314
+ protected _primaryKeys: string[];
315
+ protected _columnMap: Map<string, string>;
316
+ protected _defaults: Map<string, TDbDefaultValue>;
317
+ protected _ignoredFields: Set<string>;
318
+ protected _uniqueProps: Set<string>;
319
+ protected readonly validators: Map<string, Validator<T, DataType>>;
320
+ constructor(_type: T, adapter: BaseDbAdapter, logger?: TGenericLogger);
321
+ /** The raw annotated type. */
322
+ get type(): TAtscriptAnnotatedType<TAtscriptTypeObject>;
323
+ /** Lazily-built flat map of all fields (dot-notation paths → annotated types). */
324
+ get flatMap(): Map<string, TAtscriptAnnotatedType>;
325
+ /** All computed indexes from `@db.index.*` annotations. */
326
+ get indexes(): Map<string, TDbIndex>;
327
+ /** Primary key field names from `@meta.id`. */
328
+ get primaryKeys(): readonly string[];
329
+ /** Logical → physical column name mapping from `@db.column`. */
330
+ get columnMap(): ReadonlyMap<string, string>;
331
+ /** Default values from `@db.default.*`. */
332
+ get defaults(): ReadonlyMap<string, TDbDefaultValue>;
333
+ /** Fields excluded from DB via `@db.ignore`. */
334
+ get ignoredFields(): ReadonlySet<string>;
335
+ /** Single-field unique index properties. */
336
+ get uniqueProps(): ReadonlySet<string>;
337
+ /** Descriptor for the primary ID field(s). */
338
+ getIdDescriptor(): TIdDescriptor;
339
+ /**
340
+ * Pre-computed field metadata for adapter use.
341
+ * Filters root entry, resolves designType, physicalName, optional —
342
+ * encapsulating all the type introspection gotchas.
343
+ */
344
+ get fieldDescriptors(): readonly TDbFieldMeta[];
345
+ /**
346
+ * Resolves a projection to a list of field names to include.
347
+ * - `undefined` → `undefined` (all fields)
348
+ * - `string[]` → pass through
349
+ * - `Record<K, 1>` → extract included keys
350
+ * - `Record<K, 0>` → invert using known field names
351
+ */
352
+ resolveProjection(projection?: TDbProjection<DataType>): string[] | undefined;
353
+ /**
354
+ * Creates a new validator with custom options.
355
+ * Adapter plugins are NOT automatically included — use {@link getValidator}
356
+ * for the standard validator with adapter plugins.
357
+ */
358
+ createValidator(opts?: Partial<TValidatorOptions>): Validator<T, DataType>;
359
+ /**
360
+ * Returns a cached validator for the given purpose.
361
+ * Built with adapter plugins from {@link BaseDbAdapter.getValidatorPlugins}.
362
+ *
363
+ * Standard purposes: `'insert'`, `'update'`, `'patch'`.
364
+ * Adapters may define additional purposes.
365
+ */
366
+ getValidator(purpose: string): Validator<T, DataType>;
367
+ /**
368
+ * Inserts a single record.
369
+ * Applies defaults, validates, prepares ID, maps columns, strips ignored fields.
370
+ */
371
+ insertOne(payload: Partial<DataType> & Record<string, unknown>): Promise<TDbInsertResult>;
372
+ /**
373
+ * Inserts multiple records.
374
+ */
375
+ insertMany(payloads: Array<Partial<DataType> & Record<string, unknown>>): Promise<TDbInsertManyResult>;
376
+ /**
377
+ * Replaces a single record identified by primary key(s).
378
+ * The payload must include primary key field(s).
379
+ */
380
+ replaceOne(payload: DataType & Record<string, unknown>): Promise<TDbUpdateResult>;
381
+ /**
382
+ * Partially updates a single record identified by primary key(s).
383
+ * Supports array patch operations (`$replace`, `$insert`, `$upsert`,
384
+ * `$update`, `$remove`) for top-level array fields.
385
+ */
386
+ updateOne(payload: Partial<DataType> & Record<string, unknown>): Promise<TDbUpdateResult>;
387
+ /**
388
+ * Deletes a single record by primary key value.
389
+ */
390
+ deleteOne(id: unknown): Promise<TDbDeleteResult>;
391
+ /**
392
+ * Finds a single record matching the filter.
393
+ */
394
+ findOne(filter: TDbFilter<DataType>, options?: TDbFindOptions<DataType>): Promise<DataType | null>;
395
+ /**
396
+ * Finds all records matching the filter.
397
+ */
398
+ findMany(filter: TDbFilter<DataType>, options?: TDbFindOptions<DataType>): Promise<DataType[]>;
399
+ /**
400
+ * Counts records matching the filter.
401
+ */
402
+ count(filter?: TDbFilter<DataType>): Promise<number>;
403
+ updateMany(filter: TDbFilter<DataType>, data: Partial<DataType> & Record<string, unknown>): Promise<TDbUpdateResult>;
404
+ replaceMany(filter: TDbFilter<DataType>, data: Record<string, unknown>): Promise<TDbUpdateResult>;
405
+ deleteMany(filter: TDbFilter<DataType>): Promise<TDbDeleteResult>;
406
+ /**
407
+ * Synchronizes indexes between Atscript definitions and the database.
408
+ * Delegates to the adapter, which uses `this._table.indexes`.
409
+ */
410
+ syncIndexes(): Promise<void>;
411
+ /**
412
+ * Ensures the table/collection exists in the database.
413
+ */
414
+ ensureTable(): Promise<void>;
415
+ protected _flatten(): void;
416
+ /**
417
+ * Scans `@db.*` and `@meta.id` annotations on a field during flattening.
418
+ */
419
+ private _scanGenericAnnotations;
420
+ protected _addIndexField(type: TDbIndex['type'], name: string, field: string, sort?: 'asc' | 'desc'): void;
421
+ private _finalizeIndexes;
422
+ /**
423
+ * Applies default values for fields that are missing from the payload.
424
+ * Called before validation so that defaults satisfy required field constraints.
425
+ */
426
+ protected _applyDefaults(data: Record<string, unknown>): Record<string, unknown>;
427
+ /**
428
+ * Prepares a payload for writing to the database:
429
+ * prepares IDs, strips ignored fields, maps column names.
430
+ * Defaults should be applied before this via `_applyDefaults`.
431
+ */
432
+ protected _prepareForWrite(payload: Record<string, unknown>): Record<string, unknown>;
433
+ /**
434
+ * Extracts primary key field(s) from a payload to build a filter.
435
+ */
436
+ protected _extractPrimaryKeyFilter(payload: Record<string, unknown>): TDbFilter;
437
+ /**
438
+ * Builds a validator for a given purpose with adapter plugins.
439
+ */
440
+ protected _buildValidator(purpose: string): Validator<T, DataType>;
441
+ }
442
+
443
+ /**
444
+ * Decomposes a patch payload into a flat update object for adapters
445
+ * that don't support native patch operations.
446
+ *
447
+ * Handles:
448
+ * - Top-level array patches (`$replace`, `$insert`, `$upsert`, `$update`, `$remove`)
449
+ * - Merge strategy for nested objects
450
+ * - Simple field sets
451
+ *
452
+ * For adapters with native patch support (e.g., MongoDB aggregation pipelines),
453
+ * use {@link BaseDbAdapter.nativePatch} instead.
454
+ *
455
+ * @param payload - The patch payload from the user.
456
+ * @param table - The AtscriptDbTable instance for metadata access.
457
+ * @returns A flat update object suitable for a basic `updateOne` call.
458
+ */
459
+ declare function decomposePatch(payload: Record<string, unknown>, table: AtscriptDbTable): Record<string, unknown>;
460
+
461
+ interface TArrayPatch<A extends readonly unknown[]> {
462
+ $replace?: A;
463
+ $insert?: A;
464
+ $upsert?: A;
465
+ $update?: Array<Partial<TArrayElement<A>>>;
466
+ $remove?: Array<Partial<TArrayElement<A>>>;
467
+ }
468
+ type TArrayElement<ArrayType extends readonly unknown[]> = ArrayType extends ReadonlyArray<infer ElementType> ? ElementType : never;
469
+ /**
470
+ * Maps each property of T into a patch payload:
471
+ * - Array properties become `TArrayPatch<T[K]>`
472
+ * - Non-array properties become `Partial<T[K]>`
473
+ */
474
+ type TDbPatch<T> = {
475
+ [K in keyof T]?: T[K] extends Array<infer _> ? TArrayPatch<T[K]> : Partial<T[K]>;
476
+ };
477
+ /**
478
+ * Extracts `@expect.array.key` properties from an array-of-objects type.
479
+ * These keys uniquely identify an element inside the array and are used
480
+ * for `$update`, `$remove`, and `$upsert` operations.
481
+ *
482
+ * @param def - Atscript array type definition.
483
+ * @returns Set of property names marked as keys; empty set if none.
484
+ */
485
+ declare function getKeyProps(def: TAtscriptAnnotatedType<TAtscriptTypeArray>): Set<string>;
486
+
487
+ export { AtscriptDbTable, BaseDbAdapter, NoopLogger, decomposePatch, getKeyProps, resolveDesignType };
488
+ export type { TArrayPatch, TDbDefaultValue, TDbDeleteResult, TDbFieldMeta, TDbFilter, TDbFindOptions, TDbIndex, TDbIndexField, TDbInsertManyResult, TDbInsertResult, TDbPatch, TDbProjection, TDbUpdateResult, TFilterOperators, TGenericLogger, TIdDescriptor };