@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.
- package/LICENSE +21 -0
- package/README.md +320 -0
- package/dist/index.cjs +595 -0
- package/dist/index.d.ts +488 -0
- package/dist/index.mjs +566 -0
- package/package.json +46 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
import { flattenAnnotatedType, isAnnotatedType } from "@atscript/typescript/utils";
|
|
2
|
+
|
|
3
|
+
//#region packages/utils-db/src/logger.ts
|
|
4
|
+
const NoopLogger = {
|
|
5
|
+
error: () => {},
|
|
6
|
+
warn: () => {},
|
|
7
|
+
log: () => {},
|
|
8
|
+
info: () => {},
|
|
9
|
+
debug: () => {}
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region packages/utils-db/src/patch-types.ts
|
|
14
|
+
function getKeyProps(def) {
|
|
15
|
+
if (def.type.of.type.kind === "object") {
|
|
16
|
+
const objType = def.type.of.type;
|
|
17
|
+
const keyProps = new Set();
|
|
18
|
+
for (const [key, val] of objType.props.entries()) if (val.metadata.get("expect.array.key")) keyProps.add(key);
|
|
19
|
+
return keyProps;
|
|
20
|
+
}
|
|
21
|
+
return new Set();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region packages/utils-db/src/patch-decomposer.ts
|
|
26
|
+
function decomposePatch(payload, table) {
|
|
27
|
+
const update = {};
|
|
28
|
+
const topLevelArrayTag = "db.__topLevelArray";
|
|
29
|
+
flattenPatchPayload(payload, "", update, table, topLevelArrayTag);
|
|
30
|
+
return update;
|
|
31
|
+
}
|
|
32
|
+
function flattenPatchPayload(payload, prefix, update, table, topLevelArrayTag) {
|
|
33
|
+
for (const [_key, value] of Object.entries(payload)) {
|
|
34
|
+
const key = prefix ? `${prefix}.${_key}` : _key;
|
|
35
|
+
if (table.primaryKeys.includes(key)) continue;
|
|
36
|
+
const flatType = table.flatMap.get(key);
|
|
37
|
+
const isTopLevelArray = flatType?.metadata?.get(topLevelArrayTag);
|
|
38
|
+
if (typeof value === "object" && value !== null && isTopLevelArray) decomposeArrayPatch(key, value, flatType, update, table);
|
|
39
|
+
else if (typeof value === "object" && value !== null && !Array.isArray(value) && flatType?.metadata?.get("db.patch.strategy") === "merge") flattenPatchPayload(value, key, update, table, topLevelArrayTag);
|
|
40
|
+
else update[key] = value;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Decomposes array patch operators into simple field updates.
|
|
45
|
+
*
|
|
46
|
+
* For adapters without native array operations, this does a best-effort
|
|
47
|
+
* decomposition:
|
|
48
|
+
* - `$replace` → direct set
|
|
49
|
+
* - `$insert` → value to append (adapter must handle)
|
|
50
|
+
* - `$upsert` → value to upsert by key (adapter must handle)
|
|
51
|
+
* - `$update` → value to update by key (adapter must handle)
|
|
52
|
+
* - `$remove` → value to remove by key (adapter must handle)
|
|
53
|
+
*
|
|
54
|
+
* Note: For full correctness with `$insert`/`$upsert`/`$update`/`$remove`,
|
|
55
|
+
* the adapter should implement native patch support. This generic decomposition
|
|
56
|
+
* handles `$replace` directly and stores the other operations in a structured
|
|
57
|
+
* format the adapter can interpret.
|
|
58
|
+
*/ function decomposeArrayPatch(key, value, fieldType, update, table) {
|
|
59
|
+
const keyProps = fieldType.type.kind === "array" ? getKeyProps(fieldType) : new Set();
|
|
60
|
+
if (value.$replace !== undefined) {
|
|
61
|
+
update[key] = value.$replace;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (value.$insert !== undefined) update[`${key}.__$insert`] = value.$insert;
|
|
65
|
+
if (value.$upsert !== undefined) {
|
|
66
|
+
update[`${key}.__$upsert`] = value.$upsert;
|
|
67
|
+
if (keyProps.size > 0) update[`${key}.__$keys`] = [...keyProps];
|
|
68
|
+
}
|
|
69
|
+
if (value.$update !== undefined) {
|
|
70
|
+
update[`${key}.__$update`] = value.$update;
|
|
71
|
+
if (keyProps.size > 0) update[`${key}.__$keys`] = [...keyProps];
|
|
72
|
+
}
|
|
73
|
+
if (value.$remove !== undefined) {
|
|
74
|
+
update[`${key}.__$remove`] = value.$remove;
|
|
75
|
+
if (keyProps.size > 0) update[`${key}.__$keys`] = [...keyProps];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region packages/utils-db/src/db-table.ts
|
|
81
|
+
function _define_property$1(obj, key, value) {
|
|
82
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
83
|
+
value,
|
|
84
|
+
enumerable: true,
|
|
85
|
+
configurable: true,
|
|
86
|
+
writable: true
|
|
87
|
+
});
|
|
88
|
+
else obj[key] = value;
|
|
89
|
+
return obj;
|
|
90
|
+
}
|
|
91
|
+
const INDEX_PREFIX = "atscript__";
|
|
92
|
+
function resolveDesignType(fieldType) {
|
|
93
|
+
if (fieldType.type.kind === "") return fieldType.type.designType ?? "string";
|
|
94
|
+
if (fieldType.type.kind === "object") return "object";
|
|
95
|
+
if (fieldType.type.kind === "array") return "array";
|
|
96
|
+
return "string";
|
|
97
|
+
}
|
|
98
|
+
function indexKey(type, name) {
|
|
99
|
+
const cleanName = name.replace(/[^a-z0-9_.-]/gi, "_").replace(/_+/g, "_").slice(0, 127 - INDEX_PREFIX.length - type.length - 2);
|
|
100
|
+
return `${INDEX_PREFIX}${type}__${cleanName}`;
|
|
101
|
+
}
|
|
102
|
+
var AtscriptDbTable = class {
|
|
103
|
+
/** The raw annotated type. */ get type() {
|
|
104
|
+
return this._type;
|
|
105
|
+
}
|
|
106
|
+
/** Lazily-built flat map of all fields (dot-notation paths → annotated types). */ get flatMap() {
|
|
107
|
+
this._flatten();
|
|
108
|
+
return this._flatMap;
|
|
109
|
+
}
|
|
110
|
+
/** All computed indexes from `@db.index.*` annotations. */ get indexes() {
|
|
111
|
+
this._flatten();
|
|
112
|
+
return this._indexes;
|
|
113
|
+
}
|
|
114
|
+
/** Primary key field names from `@meta.id`. */ get primaryKeys() {
|
|
115
|
+
this._flatten();
|
|
116
|
+
return this._primaryKeys;
|
|
117
|
+
}
|
|
118
|
+
/** Logical → physical column name mapping from `@db.column`. */ get columnMap() {
|
|
119
|
+
this._flatten();
|
|
120
|
+
return this._columnMap;
|
|
121
|
+
}
|
|
122
|
+
/** Default values from `@db.default.*`. */ get defaults() {
|
|
123
|
+
this._flatten();
|
|
124
|
+
return this._defaults;
|
|
125
|
+
}
|
|
126
|
+
/** Fields excluded from DB via `@db.ignore`. */ get ignoredFields() {
|
|
127
|
+
this._flatten();
|
|
128
|
+
return this._ignoredFields;
|
|
129
|
+
}
|
|
130
|
+
/** Single-field unique index properties. */ get uniqueProps() {
|
|
131
|
+
this._flatten();
|
|
132
|
+
return this._uniqueProps;
|
|
133
|
+
}
|
|
134
|
+
/** Descriptor for the primary ID field(s). */ getIdDescriptor() {
|
|
135
|
+
this._flatten();
|
|
136
|
+
return {
|
|
137
|
+
fields: [...this._primaryKeys],
|
|
138
|
+
isComposite: this._primaryKeys.length > 1
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Pre-computed field metadata for adapter use.
|
|
143
|
+
* Filters root entry, resolves designType, physicalName, optional —
|
|
144
|
+
* encapsulating all the type introspection gotchas.
|
|
145
|
+
*/ get fieldDescriptors() {
|
|
146
|
+
this._flatten();
|
|
147
|
+
if (!this._fieldDescriptors) {
|
|
148
|
+
this._fieldDescriptors = [];
|
|
149
|
+
for (const [path, type] of this._flatMap.entries()) {
|
|
150
|
+
if (!path) continue;
|
|
151
|
+
this._fieldDescriptors.push({
|
|
152
|
+
path,
|
|
153
|
+
type,
|
|
154
|
+
physicalName: this._columnMap.get(path) ?? path,
|
|
155
|
+
designType: resolveDesignType(type),
|
|
156
|
+
optional: type.optional === true,
|
|
157
|
+
isPrimaryKey: this._primaryKeys.includes(path),
|
|
158
|
+
ignored: this._ignoredFields.has(path),
|
|
159
|
+
defaultValue: this._defaults.get(path)
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return this._fieldDescriptors;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Resolves a projection to a list of field names to include.
|
|
167
|
+
* - `undefined` → `undefined` (all fields)
|
|
168
|
+
* - `string[]` → pass through
|
|
169
|
+
* - `Record<K, 1>` → extract included keys
|
|
170
|
+
* - `Record<K, 0>` → invert using known field names
|
|
171
|
+
*/ resolveProjection(projection) {
|
|
172
|
+
if (!projection) return undefined;
|
|
173
|
+
if (Array.isArray(projection)) return projection.length > 0 ? projection : undefined;
|
|
174
|
+
const entries = Object.entries(projection);
|
|
175
|
+
if (entries.length === 0) return undefined;
|
|
176
|
+
const firstVal = entries[0][1];
|
|
177
|
+
if (firstVal === 1) return entries.filter(([, v]) => v === 1).map(([k]) => k);
|
|
178
|
+
const excluded = new Set(entries.filter(([, v]) => v === 0).map(([k]) => k));
|
|
179
|
+
return this.fieldDescriptors.filter((f) => !f.ignored && !excluded.has(f.path)).map((f) => f.path);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Creates a new validator with custom options.
|
|
183
|
+
* Adapter plugins are NOT automatically included — use {@link getValidator}
|
|
184
|
+
* for the standard validator with adapter plugins.
|
|
185
|
+
*/ createValidator(opts) {
|
|
186
|
+
return this._type.validator(opts);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Returns a cached validator for the given purpose.
|
|
190
|
+
* Built with adapter plugins from {@link BaseDbAdapter.getValidatorPlugins}.
|
|
191
|
+
*
|
|
192
|
+
* Standard purposes: `'insert'`, `'update'`, `'patch'`.
|
|
193
|
+
* Adapters may define additional purposes.
|
|
194
|
+
*/ getValidator(purpose) {
|
|
195
|
+
if (!this.validators.has(purpose)) {
|
|
196
|
+
const validator = this._buildValidator(purpose);
|
|
197
|
+
this.validators.set(purpose, validator);
|
|
198
|
+
}
|
|
199
|
+
return this.validators.get(purpose);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Inserts a single record.
|
|
203
|
+
* Applies defaults, validates, prepares ID, maps columns, strips ignored fields.
|
|
204
|
+
*/ async insertOne(payload) {
|
|
205
|
+
this._flatten();
|
|
206
|
+
const data = this._applyDefaults({ ...payload });
|
|
207
|
+
const validator = this.getValidator("insert");
|
|
208
|
+
if (!validator.validate(data)) throw new Error("Validation failed for insert");
|
|
209
|
+
return this.adapter.insertOne(this._prepareForWrite(data));
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Inserts multiple records.
|
|
213
|
+
*/ async insertMany(payloads) {
|
|
214
|
+
this._flatten();
|
|
215
|
+
const validator = this.getValidator("insert");
|
|
216
|
+
const prepared = [];
|
|
217
|
+
for (const payload of payloads) {
|
|
218
|
+
const data = this._applyDefaults({ ...payload });
|
|
219
|
+
if (!validator.validate(data)) throw new Error("Validation failed for insert");
|
|
220
|
+
prepared.push(this._prepareForWrite(data));
|
|
221
|
+
}
|
|
222
|
+
return this.adapter.insertMany(prepared);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Replaces a single record identified by primary key(s).
|
|
226
|
+
* The payload must include primary key field(s).
|
|
227
|
+
*/ async replaceOne(payload) {
|
|
228
|
+
this._flatten();
|
|
229
|
+
const validator = this.getValidator("update");
|
|
230
|
+
if (!validator.validate(payload)) throw new Error("Validation failed for replace");
|
|
231
|
+
const filter = this._extractPrimaryKeyFilter(payload);
|
|
232
|
+
const data = this._prepareForWrite({ ...payload });
|
|
233
|
+
return this.adapter.replaceOne(filter, data);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Partially updates a single record identified by primary key(s).
|
|
237
|
+
* Supports array patch operations (`$replace`, `$insert`, `$upsert`,
|
|
238
|
+
* `$update`, `$remove`) for top-level array fields.
|
|
239
|
+
*/ async updateOne(payload) {
|
|
240
|
+
this._flatten();
|
|
241
|
+
const validator = this.getValidator("patch");
|
|
242
|
+
if (!validator.validate(payload)) throw new Error("Validation failed for update");
|
|
243
|
+
const filter = this._extractPrimaryKeyFilter(payload);
|
|
244
|
+
if (this.adapter.supportsNativePatch()) return this.adapter.nativePatch(filter, payload);
|
|
245
|
+
const update = decomposePatch(payload, this);
|
|
246
|
+
return this.adapter.updateOne(filter, update);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Deletes a single record by primary key value.
|
|
250
|
+
*/ async deleteOne(id) {
|
|
251
|
+
this._flatten();
|
|
252
|
+
const pkFields = this.primaryKeys;
|
|
253
|
+
if (pkFields.length === 0) throw new Error("No primary key defined — cannot delete by ID");
|
|
254
|
+
const filter = {};
|
|
255
|
+
if (pkFields.length === 1) {
|
|
256
|
+
const field = pkFields[0];
|
|
257
|
+
const fieldType = this.flatMap.get(field);
|
|
258
|
+
filter[field] = fieldType ? this.adapter.prepareId(id, fieldType) : id;
|
|
259
|
+
} else {
|
|
260
|
+
const idObj = id;
|
|
261
|
+
for (const field of pkFields) {
|
|
262
|
+
const fieldType = this.flatMap.get(field);
|
|
263
|
+
filter[field] = fieldType ? this.adapter.prepareId(idObj[field], fieldType) : idObj[field];
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return this.adapter.deleteOne(filter);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Finds a single record matching the filter.
|
|
270
|
+
*/ async findOne(filter, options) {
|
|
271
|
+
return this.adapter.findOne(filter, options);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Finds all records matching the filter.
|
|
275
|
+
*/ async findMany(filter, options) {
|
|
276
|
+
return this.adapter.findMany(filter, options);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Counts records matching the filter.
|
|
280
|
+
*/ async count(filter = {}) {
|
|
281
|
+
return this.adapter.count(filter);
|
|
282
|
+
}
|
|
283
|
+
async updateMany(filter, data) {
|
|
284
|
+
return this.adapter.updateMany(filter, data);
|
|
285
|
+
}
|
|
286
|
+
async replaceMany(filter, data) {
|
|
287
|
+
return this.adapter.replaceMany(filter, data);
|
|
288
|
+
}
|
|
289
|
+
async deleteMany(filter) {
|
|
290
|
+
return this.adapter.deleteMany(filter);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Synchronizes indexes between Atscript definitions and the database.
|
|
294
|
+
* Delegates to the adapter, which uses `this._table.indexes`.
|
|
295
|
+
*/ async syncIndexes() {
|
|
296
|
+
this._flatten();
|
|
297
|
+
return this.adapter.syncIndexes();
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Ensures the table/collection exists in the database.
|
|
301
|
+
*/ async ensureTable() {
|
|
302
|
+
this._flatten();
|
|
303
|
+
return this.adapter.ensureTable();
|
|
304
|
+
}
|
|
305
|
+
_flatten() {
|
|
306
|
+
if (this._flatMap) return;
|
|
307
|
+
this.adapter.onBeforeFlatten?.(this._type);
|
|
308
|
+
this._flatMap = flattenAnnotatedType(this.type, {
|
|
309
|
+
topLevelArrayTag: this.adapter.getTopLevelArrayTag?.() ?? "db.__topLevelArray",
|
|
310
|
+
excludePhantomTypes: true,
|
|
311
|
+
onField: (path, type, metadata) => {
|
|
312
|
+
this._scanGenericAnnotations(path, metadata);
|
|
313
|
+
this.adapter.onFieldScanned?.(path, type, metadata);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
this._finalizeIndexes();
|
|
317
|
+
this.adapter.onAfterFlatten?.();
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Scans `@db.*` and `@meta.id` annotations on a field during flattening.
|
|
321
|
+
*/ _scanGenericAnnotations(fieldName, metadata) {
|
|
322
|
+
if (metadata.has("meta.id")) this._primaryKeys.push(fieldName);
|
|
323
|
+
const column = metadata.get("db.column");
|
|
324
|
+
if (column) this._columnMap.set(fieldName, column);
|
|
325
|
+
const defaultValue = metadata.get("db.default.value");
|
|
326
|
+
const defaultFn = metadata.get("db.default.fn");
|
|
327
|
+
if (defaultValue !== undefined) this._defaults.set(fieldName, {
|
|
328
|
+
kind: "value",
|
|
329
|
+
value: defaultValue
|
|
330
|
+
});
|
|
331
|
+
else if (defaultFn !== undefined) this._defaults.set(fieldName, {
|
|
332
|
+
kind: "fn",
|
|
333
|
+
fn: defaultFn
|
|
334
|
+
});
|
|
335
|
+
if (metadata.has("db.ignore")) this._ignoredFields.add(fieldName);
|
|
336
|
+
for (const index of metadata.get("db.index.plain") || []) {
|
|
337
|
+
const name = index === true ? fieldName : index?.name || fieldName;
|
|
338
|
+
const sort = (index === true ? undefined : index?.sort) || "asc";
|
|
339
|
+
this._addIndexField("plain", name, fieldName, sort);
|
|
340
|
+
}
|
|
341
|
+
for (const index of metadata.get("db.index.unique") || []) {
|
|
342
|
+
const name = index === true ? fieldName : typeof index === "string" ? index : index?.name || fieldName;
|
|
343
|
+
this._addIndexField("unique", name, fieldName);
|
|
344
|
+
}
|
|
345
|
+
for (const index of metadata.get("db.index.fulltext") || []) {
|
|
346
|
+
const name = index === true ? fieldName : typeof index === "string" ? index : index?.name || fieldName;
|
|
347
|
+
this._addIndexField("fulltext", name, fieldName);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
_addIndexField(type, name, field, sort = "asc") {
|
|
351
|
+
const key = indexKey(type, name);
|
|
352
|
+
const index = this._indexes.get(key);
|
|
353
|
+
const indexField = {
|
|
354
|
+
name: field,
|
|
355
|
+
sort
|
|
356
|
+
};
|
|
357
|
+
if (index) index.fields.push(indexField);
|
|
358
|
+
else this._indexes.set(key, {
|
|
359
|
+
key,
|
|
360
|
+
name,
|
|
361
|
+
type,
|
|
362
|
+
fields: [indexField]
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
_finalizeIndexes() {
|
|
366
|
+
for (const index of this._indexes.values()) if (index.type === "unique" && index.fields.length === 1) this._uniqueProps.add(index.fields[0].name);
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Applies default values for fields that are missing from the payload.
|
|
370
|
+
* Called before validation so that defaults satisfy required field constraints.
|
|
371
|
+
*/ _applyDefaults(data) {
|
|
372
|
+
for (const [field, def] of this._defaults.entries()) if (data[field] === undefined) {
|
|
373
|
+
if (def.kind === "value") data[field] = def.value;
|
|
374
|
+
}
|
|
375
|
+
return data;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Prepares a payload for writing to the database:
|
|
379
|
+
* prepares IDs, strips ignored fields, maps column names.
|
|
380
|
+
* Defaults should be applied before this via `_applyDefaults`.
|
|
381
|
+
*/ _prepareForWrite(payload) {
|
|
382
|
+
const data = { ...payload };
|
|
383
|
+
for (const pk of this._primaryKeys) if (data[pk] !== undefined) {
|
|
384
|
+
const fieldType = this._flatMap?.get(pk);
|
|
385
|
+
if (fieldType) data[pk] = this.adapter.prepareId(data[pk], fieldType);
|
|
386
|
+
}
|
|
387
|
+
for (const field of this._ignoredFields) delete data[field];
|
|
388
|
+
for (const [logical, physical] of this._columnMap.entries()) if (logical in data) {
|
|
389
|
+
data[physical] = data[logical];
|
|
390
|
+
delete data[logical];
|
|
391
|
+
}
|
|
392
|
+
return data;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Extracts primary key field(s) from a payload to build a filter.
|
|
396
|
+
*/ _extractPrimaryKeyFilter(payload) {
|
|
397
|
+
const pkFields = this.primaryKeys;
|
|
398
|
+
if (pkFields.length === 0) throw new Error("No primary key defined — cannot extract filter");
|
|
399
|
+
const filter = {};
|
|
400
|
+
for (const field of pkFields) {
|
|
401
|
+
if (payload[field] === undefined) throw new Error(`Missing primary key field "${field}" in payload`);
|
|
402
|
+
const fieldType = this.flatMap.get(field);
|
|
403
|
+
filter[field] = fieldType ? this.adapter.prepareId(payload[field], fieldType) : payload[field];
|
|
404
|
+
}
|
|
405
|
+
return filter;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Builds a validator for a given purpose with adapter plugins.
|
|
409
|
+
*/ _buildValidator(purpose) {
|
|
410
|
+
const plugins = this.adapter.getValidatorPlugins();
|
|
411
|
+
switch (purpose) {
|
|
412
|
+
case "insert": return this.createValidator({
|
|
413
|
+
plugins,
|
|
414
|
+
replace: (type, path) => {
|
|
415
|
+
if (this._primaryKeys.includes(path)) return {
|
|
416
|
+
...type,
|
|
417
|
+
optional: true
|
|
418
|
+
};
|
|
419
|
+
return type;
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
case "update": return this.createValidator({ plugins });
|
|
423
|
+
case "patch": return this.createValidator({
|
|
424
|
+
plugins,
|
|
425
|
+
partial: true
|
|
426
|
+
});
|
|
427
|
+
default: return this.createValidator({ plugins });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
constructor(_type, adapter, logger = NoopLogger) {
|
|
431
|
+
_define_property$1(this, "_type", void 0);
|
|
432
|
+
_define_property$1(this, "adapter", void 0);
|
|
433
|
+
_define_property$1(this, "logger", void 0);
|
|
434
|
+
/** Resolved table/collection name. */ _define_property$1(this, "tableName", void 0);
|
|
435
|
+
/** Database schema/namespace from `@db.schema` (if set). */ _define_property$1(this, "schema", void 0);
|
|
436
|
+
_define_property$1(this, "_flatMap", void 0);
|
|
437
|
+
_define_property$1(this, "_fieldDescriptors", void 0);
|
|
438
|
+
_define_property$1(this, "_indexes", void 0);
|
|
439
|
+
_define_property$1(this, "_primaryKeys", void 0);
|
|
440
|
+
_define_property$1(this, "_columnMap", void 0);
|
|
441
|
+
_define_property$1(this, "_defaults", void 0);
|
|
442
|
+
_define_property$1(this, "_ignoredFields", void 0);
|
|
443
|
+
_define_property$1(this, "_uniqueProps", void 0);
|
|
444
|
+
_define_property$1(this, "validators", void 0);
|
|
445
|
+
this._type = _type;
|
|
446
|
+
this.adapter = adapter;
|
|
447
|
+
this.logger = logger;
|
|
448
|
+
this._indexes = new Map();
|
|
449
|
+
this._primaryKeys = [];
|
|
450
|
+
this._columnMap = new Map();
|
|
451
|
+
this._defaults = new Map();
|
|
452
|
+
this._ignoredFields = new Set();
|
|
453
|
+
this._uniqueProps = new Set();
|
|
454
|
+
this.validators = new Map();
|
|
455
|
+
if (!isAnnotatedType(_type)) throw new Error("Atscript Annotated Type expected");
|
|
456
|
+
if (_type.type.kind !== "object") throw new Error("Database table type must be an object type");
|
|
457
|
+
const adapterName = adapter.getAdapterTableName?.(_type);
|
|
458
|
+
const dbTable = _type.metadata.get("db.table");
|
|
459
|
+
const fallbackName = _type.id || "";
|
|
460
|
+
this.tableName = adapterName || dbTable || fallbackName;
|
|
461
|
+
if (!this.tableName) throw new Error("@db.table annotation or adapter-specific table name expected");
|
|
462
|
+
this.schema = _type.metadata.get("db.schema");
|
|
463
|
+
adapter.registerTable(this);
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
//#endregion
|
|
468
|
+
//#region packages/utils-db/src/base-adapter.ts
|
|
469
|
+
function _define_property(obj, key, value) {
|
|
470
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
471
|
+
value,
|
|
472
|
+
enumerable: true,
|
|
473
|
+
configurable: true,
|
|
474
|
+
writable: true
|
|
475
|
+
});
|
|
476
|
+
else obj[key] = value;
|
|
477
|
+
return obj;
|
|
478
|
+
}
|
|
479
|
+
var BaseDbAdapter = class {
|
|
480
|
+
/**
|
|
481
|
+
* Called by {@link AtscriptDbTable} constructor. Gives the adapter access to
|
|
482
|
+
* the table's computed metadata for internal use in query rendering, index
|
|
483
|
+
* sync, etc.
|
|
484
|
+
*/ registerTable(table) {
|
|
485
|
+
this._table = table;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Returns additional validator plugins for this adapter.
|
|
489
|
+
* These are merged with the built-in Atscript validators.
|
|
490
|
+
*
|
|
491
|
+
* Example: MongoDB adapter returns ObjectId validation plugin.
|
|
492
|
+
*/ getValidatorPlugins() {
|
|
493
|
+
return [];
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Transforms an ID value for the database.
|
|
497
|
+
* Override to convert string → ObjectId, parse numeric IDs, etc.
|
|
498
|
+
*
|
|
499
|
+
* @param id - The raw ID value.
|
|
500
|
+
* @param fieldType - The annotated type of the ID field.
|
|
501
|
+
* @returns The transformed ID value.
|
|
502
|
+
*/ prepareId(id, fieldType) {
|
|
503
|
+
return id;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Whether this adapter supports native patch operations.
|
|
507
|
+
* When `true`, {@link AtscriptDbTable} delegates patch payloads to
|
|
508
|
+
* {@link nativePatch} instead of using the generic decomposition.
|
|
509
|
+
*/ supportsNativePatch() {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Applies a patch payload using native database operations.
|
|
514
|
+
* Only called when {@link supportsNativePatch} returns `true`.
|
|
515
|
+
*
|
|
516
|
+
* @param filter - Filter identifying the record to patch.
|
|
517
|
+
* @param patch - The patch payload with array operations.
|
|
518
|
+
* @returns Update result.
|
|
519
|
+
*/ async nativePatch(filter, patch) {
|
|
520
|
+
throw new Error("Native patch not supported by this adapter");
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Resolves the full table name, optionally including the schema prefix.
|
|
524
|
+
* Override for databases that don't support schemas (e.g., SQLite).
|
|
525
|
+
*
|
|
526
|
+
* @param includeSchema - Whether to prepend `schema.` prefix (default: true).
|
|
527
|
+
*/ resolveTableName(includeSchema = true) {
|
|
528
|
+
const schema = this._table.schema;
|
|
529
|
+
const name = this._table.tableName;
|
|
530
|
+
return includeSchema && schema ? `${schema}.${name}` : name;
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Template method for index synchronization.
|
|
534
|
+
* Implements the diff algorithm (list → compare → create/drop).
|
|
535
|
+
* Adapters provide the three DB-specific primitives.
|
|
536
|
+
*
|
|
537
|
+
* @example
|
|
538
|
+
* ```typescript
|
|
539
|
+
* async syncIndexes() {
|
|
540
|
+
* await this.syncIndexesWithDiff({
|
|
541
|
+
* listExisting: async () => this.driver.all('PRAGMA index_list(...)'),
|
|
542
|
+
* createIndex: async (index) => this.driver.exec('CREATE INDEX ...'),
|
|
543
|
+
* dropIndex: async (name) => this.driver.exec('DROP INDEX ...'),
|
|
544
|
+
* shouldSkipType: (type) => type === 'fulltext',
|
|
545
|
+
* })
|
|
546
|
+
* }
|
|
547
|
+
* ```
|
|
548
|
+
*/ async syncIndexesWithDiff(opts) {
|
|
549
|
+
const prefix = opts.prefix ?? "atscript__";
|
|
550
|
+
const existing = await opts.listExisting();
|
|
551
|
+
const existingNames = new Set(existing.filter((i) => i.name.startsWith(prefix)).map((i) => i.name));
|
|
552
|
+
const desiredNames = new Set();
|
|
553
|
+
for (const index of this._table.indexes.values()) {
|
|
554
|
+
if (opts.shouldSkipType?.(index.type)) continue;
|
|
555
|
+
desiredNames.add(index.key);
|
|
556
|
+
if (!existingNames.has(index.key)) await opts.createIndex(index);
|
|
557
|
+
}
|
|
558
|
+
for (const name of existingNames) if (!desiredNames.has(name)) await opts.dropIndex(name);
|
|
559
|
+
}
|
|
560
|
+
constructor() {
|
|
561
|
+
_define_property(this, "_table", void 0);
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
//#endregion
|
|
566
|
+
export { AtscriptDbTable, BaseDbAdapter, NoopLogger, decomposePatch, getKeyProps, resolveDesignType };
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atscript/utils-db",
|
|
3
|
+
"version": "0.1.28",
|
|
4
|
+
"description": "Database adapter utilities for atscript.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"atscript",
|
|
7
|
+
"database",
|
|
8
|
+
"utils"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://github.com/moostjs/atscript/tree/main/packages/utils-db#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/moostjs/atscript/issues"
|
|
13
|
+
},
|
|
14
|
+
"license": "ISC",
|
|
15
|
+
"author": "Artem Maltsev",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/moostjs/atscript.git",
|
|
19
|
+
"directory": "packages/utils-db"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "dist/index.mjs",
|
|
26
|
+
"types": "dist/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.mjs",
|
|
31
|
+
"require": "./dist/index.cjs"
|
|
32
|
+
},
|
|
33
|
+
"./package.json": "./package.json"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"vitest": "3.2.4"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@atscript/core": "^0.1.28",
|
|
40
|
+
"@atscript/typescript": "^0.1.28"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"pub": "pnpm publish --access public",
|
|
44
|
+
"test": "vitest"
|
|
45
|
+
}
|
|
46
|
+
}
|