@atscript/moost-db 0.1.34 → 0.1.36
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 +1 -1
- package/dist/index.cjs +237 -86
- package/dist/index.d.ts +88 -32
- package/dist/index.mjs +230 -88
- package/package.json +7 -7
package/LICENSE
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -26,19 +26,32 @@ const __atscript_typescript_utils = __toESM(require("@atscript/typescript/utils"
|
|
|
26
26
|
const __moostjs_event_http = __toESM(require("@moostjs/event-http"));
|
|
27
27
|
const moost = __toESM(require("moost"));
|
|
28
28
|
const __uniqu_url = __toESM(require("@uniqu/url"));
|
|
29
|
+
const __atscript_utils_db = __toESM(require("@atscript/utils-db"));
|
|
29
30
|
|
|
30
31
|
//#region packages/moost-db/src/decorators.ts
|
|
31
|
-
const
|
|
32
|
+
const READABLE_DEF = "__atscript_db_readable_def";
|
|
33
|
+
const TABLE_DEF = READABLE_DEF;
|
|
32
34
|
const TableController = (table, prefix) => (0, moost.ApplyDecorators)((0, moost.Provide)(TABLE_DEF, () => table), (0, moost.Controller)(prefix || table.tableName), (0, moost.Inherit)());
|
|
35
|
+
const ReadableController = (readable, prefix) => (0, moost.ApplyDecorators)((0, moost.Provide)(READABLE_DEF, () => readable), (0, moost.Controller)(prefix || readable.tableName), (0, moost.Inherit)());
|
|
36
|
+
const ViewController = ReadableController;
|
|
33
37
|
|
|
34
38
|
//#endregion
|
|
35
39
|
//#region packages/moost-db/src/validation-interceptor.ts
|
|
40
|
+
const dbErrorCodeToStatus = { CONFLICT: 409 };
|
|
36
41
|
function transformValidationError(error, reply) {
|
|
37
42
|
if (error instanceof __atscript_typescript_utils.ValidatorError) reply(new __moostjs_event_http.HttpError(400, {
|
|
38
43
|
message: error.message,
|
|
39
44
|
statusCode: 400,
|
|
40
45
|
errors: error.errors
|
|
41
46
|
}));
|
|
47
|
+
else if (error instanceof __atscript_utils_db.DbError) {
|
|
48
|
+
const statusCode = dbErrorCodeToStatus[error.code] ?? 400;
|
|
49
|
+
reply(new __moostjs_event_http.HttpError(statusCode, {
|
|
50
|
+
message: error.message,
|
|
51
|
+
statusCode,
|
|
52
|
+
errors: error.errors
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
42
55
|
}
|
|
43
56
|
const validationErrorTransform = () => (0, moost.defineInterceptor)({ error: transformValidationError }, moost.TInterceptorPriority.CATCH_ERROR);
|
|
44
57
|
const UseValidationErrorTransform = () => (0, moost.Intercept)(validationErrorTransform());
|
|
@@ -145,7 +158,7 @@ _define_property$1(SelectControlDto, "id", "SelectControlDto");
|
|
|
145
158
|
(0, __atscript_typescript_utils.defineAnnotatedType)("object", SelectControlDto).propPattern(/./, (0, __atscript_typescript_utils.defineAnnotatedType)("union").item((0, __atscript_typescript_utils.defineAnnotatedType)().designType("number").value(1).$type).item((0, __atscript_typescript_utils.defineAnnotatedType)().designType("number").value(0).$type).$type);
|
|
146
159
|
|
|
147
160
|
//#endregion
|
|
148
|
-
//#region packages/moost-db/src/as-db.controller.ts
|
|
161
|
+
//#region packages/moost-db/src/as-db-readable.controller.ts
|
|
149
162
|
function _define_property(obj, key, value) {
|
|
150
163
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
151
164
|
value,
|
|
@@ -156,21 +169,21 @@ function _define_property(obj, key, value) {
|
|
|
156
169
|
else obj[key] = value;
|
|
157
170
|
return obj;
|
|
158
171
|
}
|
|
159
|
-
function _ts_decorate(decorators, target, key, desc) {
|
|
172
|
+
function _ts_decorate$1(decorators, target, key, desc) {
|
|
160
173
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
161
174
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
162
175
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
163
176
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
164
177
|
}
|
|
165
|
-
function _ts_metadata(k, v) {
|
|
178
|
+
function _ts_metadata$1(k, v) {
|
|
166
179
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
167
180
|
}
|
|
168
|
-
function _ts_param(paramIndex, decorator) {
|
|
181
|
+
function _ts_param$1(paramIndex, decorator) {
|
|
169
182
|
return function(target, key) {
|
|
170
183
|
decorator(target, key, paramIndex);
|
|
171
184
|
};
|
|
172
185
|
}
|
|
173
|
-
var
|
|
186
|
+
var AsDbReadableController = class {
|
|
174
187
|
/**
|
|
175
188
|
* One-time initialization hook. Override to seed data, register watchers, etc.
|
|
176
189
|
*/ init() {}
|
|
@@ -192,7 +205,7 @@ var AsDbController = class {
|
|
|
192
205
|
return undefined;
|
|
193
206
|
}
|
|
194
207
|
validateInsights(insights) {
|
|
195
|
-
for (const key of insights.keys()) if (!this.
|
|
208
|
+
for (const key of insights.keys()) if (!this.readable.flatMap.has(key)) return `Unknown field "${key}"`;
|
|
196
209
|
return undefined;
|
|
197
210
|
}
|
|
198
211
|
validateParsed(parsed, type) {
|
|
@@ -202,6 +215,18 @@ var AsDbController = class {
|
|
|
202
215
|
const insightsError = this.validateInsights(parsed.insights);
|
|
203
216
|
if (insightsError) return new __moostjs_event_http.HttpError(400, insightsError);
|
|
204
217
|
}
|
|
218
|
+
const withRelations = parsed.controls.$with;
|
|
219
|
+
if (withRelations?.length) {
|
|
220
|
+
const relations = this.readable.relations;
|
|
221
|
+
for (const rel of withRelations) if (!rel.name.includes(".") && !relations.has(rel.name)) return new __moostjs_event_http.HttpError(400, {
|
|
222
|
+
message: `Unknown relation "${rel.name}" in $with. Available relations: ${[...relations.keys()].join(", ") || "(none)"}`,
|
|
223
|
+
statusCode: 400,
|
|
224
|
+
errors: [{
|
|
225
|
+
path: "$with",
|
|
226
|
+
message: `Unknown relation "${rel.name}"`
|
|
227
|
+
}]
|
|
228
|
+
});
|
|
229
|
+
}
|
|
205
230
|
return undefined;
|
|
206
231
|
}
|
|
207
232
|
/**
|
|
@@ -214,16 +239,6 @@ var AsDbController = class {
|
|
|
214
239
|
*/ transformProjection(projection) {
|
|
215
240
|
return projection;
|
|
216
241
|
}
|
|
217
|
-
/**
|
|
218
|
-
* Intercepts write operations. Return `undefined` to abort.
|
|
219
|
-
*/ onWrite(action, data) {
|
|
220
|
-
return data;
|
|
221
|
-
}
|
|
222
|
-
/**
|
|
223
|
-
* Intercepts delete operations. Return `undefined` to abort.
|
|
224
|
-
*/ onRemove(id) {
|
|
225
|
-
return id;
|
|
226
|
-
}
|
|
227
242
|
parseQueryString(url) {
|
|
228
243
|
const idx = url.indexOf("?");
|
|
229
244
|
return (0, __uniqu_url.parseUrl)(idx >= 0 ? url.slice(idx + 1) : "");
|
|
@@ -234,6 +249,38 @@ var AsDbController = class {
|
|
|
234
249
|
return item;
|
|
235
250
|
}
|
|
236
251
|
/**
|
|
252
|
+
* Extracts a composite identifier object from query params.
|
|
253
|
+
* Tries composite primary key first, then compound unique indexes.
|
|
254
|
+
*/ extractCompositeId(query) {
|
|
255
|
+
const pkFields = this.readable.primaryKeys;
|
|
256
|
+
if (pkFields.length > 1) {
|
|
257
|
+
const idObj = {};
|
|
258
|
+
let allPresent = true;
|
|
259
|
+
for (const field of pkFields) {
|
|
260
|
+
if (query[field] === undefined) {
|
|
261
|
+
allPresent = false;
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
idObj[field] = query[field];
|
|
265
|
+
}
|
|
266
|
+
if (allPresent) return idObj;
|
|
267
|
+
}
|
|
268
|
+
for (const index of this.readable.indexes.values()) {
|
|
269
|
+
if (index.type !== "unique" || index.fields.length < 2) continue;
|
|
270
|
+
const idObj = {};
|
|
271
|
+
let allPresent = true;
|
|
272
|
+
for (const indexField of index.fields) {
|
|
273
|
+
if (query[indexField.name] === undefined) {
|
|
274
|
+
allPresent = false;
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
idObj[indexField.name] = query[indexField.name];
|
|
278
|
+
}
|
|
279
|
+
if (allPresent) return idObj;
|
|
280
|
+
}
|
|
281
|
+
return new __moostjs_event_http.HttpError(400, "Query params do not match any composite primary key or compound unique index");
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
237
284
|
* **GET /query** — returns an array of records or a count.
|
|
238
285
|
*/ async query(url) {
|
|
239
286
|
const parsed = this.parseQueryString(url);
|
|
@@ -242,7 +289,7 @@ var AsDbController = class {
|
|
|
242
289
|
const controls = parsed.controls;
|
|
243
290
|
const filter = this.transformFilter(parsed.filter);
|
|
244
291
|
const select = this.transformProjection(controls.$select);
|
|
245
|
-
if (controls.$count) return this.
|
|
292
|
+
if (controls.$count) return this.readable.count({
|
|
246
293
|
filter,
|
|
247
294
|
controls: {
|
|
248
295
|
...controls,
|
|
@@ -251,7 +298,7 @@ var AsDbController = class {
|
|
|
251
298
|
});
|
|
252
299
|
const searchTerm = controls.$search;
|
|
253
300
|
const indexName = controls.$index;
|
|
254
|
-
if (searchTerm && this.
|
|
301
|
+
if (searchTerm && this.readable.isSearchable()) return this.readable.search(searchTerm, {
|
|
255
302
|
filter,
|
|
256
303
|
controls: {
|
|
257
304
|
...controls,
|
|
@@ -259,7 +306,7 @@ var AsDbController = class {
|
|
|
259
306
|
$limit: controls.$limit || 1e3
|
|
260
307
|
}
|
|
261
308
|
}, indexName);
|
|
262
|
-
return this.
|
|
309
|
+
return this.readable.findMany({
|
|
263
310
|
filter,
|
|
264
311
|
controls: {
|
|
265
312
|
...controls,
|
|
@@ -292,8 +339,8 @@ var AsDbController = class {
|
|
|
292
339
|
}
|
|
293
340
|
};
|
|
294
341
|
let result;
|
|
295
|
-
if (searchTerm && this.
|
|
296
|
-
else result = await this.
|
|
342
|
+
if (searchTerm && this.readable.isSearchable()) result = await this.readable.searchWithCount(searchTerm, query, indexName);
|
|
343
|
+
else result = await this.readable.findManyWithCount(query);
|
|
297
344
|
return {
|
|
298
345
|
data: result.data,
|
|
299
346
|
page,
|
|
@@ -310,31 +357,164 @@ else result = await this.table.findManyWithCount(query);
|
|
|
310
357
|
const error = this.validateParsed(parsed, "getOne");
|
|
311
358
|
if (error) return error;
|
|
312
359
|
const select = this.transformProjection(parsed.controls.$select);
|
|
313
|
-
|
|
360
|
+
const controls = {
|
|
361
|
+
...parsed.controls,
|
|
362
|
+
$select: select
|
|
363
|
+
};
|
|
364
|
+
return this.returnOne(this.readable.findById(id, { controls }));
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* **GET /one?field1=val1&field2=val2** — retrieves a single record by composite key
|
|
368
|
+
* (composite primary key or compound unique index).
|
|
369
|
+
*/ async getOneComposite(query, url) {
|
|
370
|
+
const idObj = this.extractCompositeId(query);
|
|
371
|
+
if (idObj instanceof __moostjs_event_http.HttpError) return idObj;
|
|
372
|
+
const parsed = this.parseQueryString(url);
|
|
373
|
+
const select = this.transformProjection(parsed.controls.$select);
|
|
374
|
+
const controls = {
|
|
375
|
+
...parsed.controls,
|
|
376
|
+
$select: select
|
|
377
|
+
};
|
|
378
|
+
return this.returnOne(this.readable.findById(idObj, { controls }));
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* **GET /meta** — returns table/view metadata for UI.
|
|
382
|
+
*/ meta() {
|
|
383
|
+
return {
|
|
384
|
+
searchable: this.readable.isSearchable(),
|
|
385
|
+
searchIndexes: this._searchIndexes,
|
|
386
|
+
type: this._serializedType
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
constructor(readable, app) {
|
|
390
|
+
/** Reference to the underlying readable (table or view). */ _define_property(this, "readable", void 0);
|
|
391
|
+
/** Application-scoped logger. */ _define_property(this, "logger", void 0);
|
|
392
|
+
/** Cached serialized type definition (static, computed once). */ _define_property(this, "_serializedType", void 0);
|
|
393
|
+
/** Cached search index list (static, computed once). */ _define_property(this, "_searchIndexes", void 0);
|
|
394
|
+
_define_property(this, "_queryControlsValidator", void 0);
|
|
395
|
+
_define_property(this, "_pagesControlsValidator", void 0);
|
|
396
|
+
_define_property(this, "_getOneControlsValidator", void 0);
|
|
397
|
+
this.readable = readable;
|
|
398
|
+
this._serializedType = (0, __atscript_typescript_utils.serializeAnnotatedType)(readable.type);
|
|
399
|
+
this._searchIndexes = readable.getSearchIndexes();
|
|
400
|
+
this.logger = app.getLogger(`db [${readable.tableName}]`);
|
|
401
|
+
this.logger.info(`Initializing ${readable.isView ? "view" : "table"} controller`);
|
|
402
|
+
try {
|
|
403
|
+
const p = this.init();
|
|
404
|
+
if (p instanceof Promise) p.catch((error) => {
|
|
405
|
+
this.logger.error(error);
|
|
406
|
+
});
|
|
407
|
+
} catch (error) {
|
|
408
|
+
this.logger.error(error);
|
|
409
|
+
throw error;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
_ts_decorate$1([
|
|
414
|
+
(0, __moostjs_event_http.Get)("query"),
|
|
415
|
+
_ts_param$1(0, (0, __moostjs_event_http.Url)()),
|
|
416
|
+
_ts_metadata$1("design:type", Function),
|
|
417
|
+
_ts_metadata$1("design:paramtypes", [String]),
|
|
418
|
+
_ts_metadata$1("design:returntype", Promise)
|
|
419
|
+
], AsDbReadableController.prototype, "query", null);
|
|
420
|
+
_ts_decorate$1([
|
|
421
|
+
(0, __moostjs_event_http.Get)("pages"),
|
|
422
|
+
_ts_param$1(0, (0, __moostjs_event_http.Url)()),
|
|
423
|
+
_ts_metadata$1("design:type", Function),
|
|
424
|
+
_ts_metadata$1("design:paramtypes", [String]),
|
|
425
|
+
_ts_metadata$1("design:returntype", Promise)
|
|
426
|
+
], AsDbReadableController.prototype, "pages", null);
|
|
427
|
+
_ts_decorate$1([
|
|
428
|
+
(0, __moostjs_event_http.Get)("one/:id"),
|
|
429
|
+
_ts_param$1(0, (0, moost.Param)("id")),
|
|
430
|
+
_ts_param$1(1, (0, __moostjs_event_http.Url)()),
|
|
431
|
+
_ts_metadata$1("design:type", Function),
|
|
432
|
+
_ts_metadata$1("design:paramtypes", [String, String]),
|
|
433
|
+
_ts_metadata$1("design:returntype", Promise)
|
|
434
|
+
], AsDbReadableController.prototype, "getOne", null);
|
|
435
|
+
_ts_decorate$1([
|
|
436
|
+
(0, __moostjs_event_http.Get)("one"),
|
|
437
|
+
_ts_param$1(0, (0, __moostjs_event_http.Query)()),
|
|
438
|
+
_ts_param$1(1, (0, __moostjs_event_http.Url)()),
|
|
439
|
+
_ts_metadata$1("design:type", Function),
|
|
440
|
+
_ts_metadata$1("design:paramtypes", [typeof Record === "undefined" ? Object : Record, String]),
|
|
441
|
+
_ts_metadata$1("design:returntype", Promise)
|
|
442
|
+
], AsDbReadableController.prototype, "getOneComposite", null);
|
|
443
|
+
_ts_decorate$1([
|
|
444
|
+
(0, __moostjs_event_http.Get)("meta"),
|
|
445
|
+
_ts_metadata$1("design:type", Function),
|
|
446
|
+
_ts_metadata$1("design:paramtypes", []),
|
|
447
|
+
_ts_metadata$1("design:returntype", void 0)
|
|
448
|
+
], AsDbReadableController.prototype, "meta", null);
|
|
449
|
+
AsDbReadableController = _ts_decorate$1([
|
|
450
|
+
UseValidationErrorTransform(),
|
|
451
|
+
_ts_param$1(0, (0, moost.Inject)(READABLE_DEF)),
|
|
452
|
+
_ts_metadata$1("design:type", Function),
|
|
453
|
+
_ts_metadata$1("design:paramtypes", [typeof AtscriptDbReadable === "undefined" ? Object : AtscriptDbReadable, typeof moost.Moost === "undefined" ? Object : moost.Moost])
|
|
454
|
+
], AsDbReadableController);
|
|
455
|
+
|
|
456
|
+
//#endregion
|
|
457
|
+
//#region packages/moost-db/src/as-db.controller.ts
|
|
458
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
459
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
460
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
461
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
462
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
463
|
+
}
|
|
464
|
+
function _ts_metadata(k, v) {
|
|
465
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
466
|
+
}
|
|
467
|
+
function _ts_param(paramIndex, decorator) {
|
|
468
|
+
return function(target, key) {
|
|
469
|
+
decorator(target, key, paramIndex);
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
var AsDbController = class extends AsDbReadableController {
|
|
473
|
+
/** Reference to the underlying table (typed for write access). */ get table() {
|
|
474
|
+
return this.readable;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Intercepts write operations. Return `undefined` to abort.
|
|
478
|
+
*/ onWrite(action, data) {
|
|
479
|
+
return data;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Intercepts delete operations. Return `undefined` to abort.
|
|
483
|
+
*/ onRemove(id) {
|
|
484
|
+
return id;
|
|
314
485
|
}
|
|
315
486
|
/**
|
|
316
487
|
* **POST /** — inserts one or many records.
|
|
317
488
|
*/ async insert(payload) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const data$1 = await this.onWrite("insert", arr[0]);
|
|
489
|
+
if (Array.isArray(payload)) {
|
|
490
|
+
const data$1 = await this.onWrite("insertMany", payload);
|
|
321
491
|
if (data$1 === undefined) return new __moostjs_event_http.HttpError(500, "Not saved");
|
|
322
|
-
return await this.table.
|
|
492
|
+
return await this.table.insertMany(data$1);
|
|
323
493
|
}
|
|
324
|
-
const data = await this.onWrite("
|
|
494
|
+
const data = await this.onWrite("insert", payload);
|
|
325
495
|
if (data === undefined) return new __moostjs_event_http.HttpError(500, "Not saved");
|
|
326
|
-
return await this.table.
|
|
496
|
+
return await this.table.insertOne(data);
|
|
327
497
|
}
|
|
328
498
|
/**
|
|
329
|
-
* **PUT /** — fully replaces
|
|
499
|
+
* **PUT /** — fully replaces one or many records matched by primary key.
|
|
330
500
|
*/ async replace(payload) {
|
|
501
|
+
if (Array.isArray(payload)) {
|
|
502
|
+
const data$1 = await this.onWrite("replaceMany", payload);
|
|
503
|
+
if (data$1 === undefined) return new __moostjs_event_http.HttpError(500, "Not saved");
|
|
504
|
+
return await this.table.bulkReplace(data$1);
|
|
505
|
+
}
|
|
331
506
|
const data = await this.onWrite("replace", payload);
|
|
332
507
|
if (data === undefined) return new __moostjs_event_http.HttpError(500, "Not saved");
|
|
333
508
|
return await this.table.replaceOne(data);
|
|
334
509
|
}
|
|
335
510
|
/**
|
|
336
|
-
* **PATCH /** — partially updates
|
|
511
|
+
* **PATCH /** — partially updates one or many records matched by primary key.
|
|
337
512
|
*/ async update(payload) {
|
|
513
|
+
if (Array.isArray(payload)) {
|
|
514
|
+
const data$1 = await this.onWrite("updateMany", payload);
|
|
515
|
+
if (data$1 === undefined) return new __moostjs_event_http.HttpError(500, "Not saved");
|
|
516
|
+
return await this.table.bulkUpdate(data$1);
|
|
517
|
+
}
|
|
338
518
|
const data = await this.onWrite("update", payload);
|
|
339
519
|
if (data === undefined) return new __moostjs_event_http.HttpError(500, "Not saved");
|
|
340
520
|
return await this.table.updateOne(data);
|
|
@@ -349,60 +529,21 @@ else result = await this.table.findManyWithCount(query);
|
|
|
349
529
|
return result;
|
|
350
530
|
}
|
|
351
531
|
/**
|
|
352
|
-
* **
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
532
|
+
* **DELETE /?field1=val1&field2=val2** — removes a record by composite key
|
|
533
|
+
* (composite primary key or compound unique index).
|
|
534
|
+
*/ async removeComposite(query) {
|
|
535
|
+
const idObj = this.extractCompositeId(query);
|
|
536
|
+
if (idObj instanceof __moostjs_event_http.HttpError) return idObj;
|
|
537
|
+
const resolvedId = await this.onRemove(idObj);
|
|
538
|
+
if (resolvedId === undefined) return new __moostjs_event_http.HttpError(500, "Not deleted");
|
|
539
|
+
const result = await this.table.deleteOne(resolvedId);
|
|
540
|
+
if (result.deletedCount < 1) return new __moostjs_event_http.HttpError(404);
|
|
541
|
+
return result;
|
|
359
542
|
}
|
|
360
543
|
constructor(table, app) {
|
|
361
|
-
|
|
362
|
-
/** Application-scoped logger. */ _define_property(this, "logger", void 0);
|
|
363
|
-
/** Cached serialized type definition (static, computed once). */ _define_property(this, "_serializedType", void 0);
|
|
364
|
-
/** Cached search index list (static, computed once). */ _define_property(this, "_searchIndexes", void 0);
|
|
365
|
-
_define_property(this, "_queryControlsValidator", void 0);
|
|
366
|
-
_define_property(this, "_pagesControlsValidator", void 0);
|
|
367
|
-
_define_property(this, "_getOneControlsValidator", void 0);
|
|
368
|
-
this.table = table;
|
|
369
|
-
this._serializedType = (0, __atscript_typescript_utils.serializeAnnotatedType)(table.type);
|
|
370
|
-
this._searchIndexes = table.getSearchIndexes();
|
|
371
|
-
this.logger = app.getLogger(`db [${table.tableName}]`);
|
|
372
|
-
this.logger.info("Initializing table controller");
|
|
373
|
-
try {
|
|
374
|
-
const p = this.init();
|
|
375
|
-
if (p instanceof Promise) p.catch((error) => {
|
|
376
|
-
this.logger.error(error);
|
|
377
|
-
});
|
|
378
|
-
} catch (error) {
|
|
379
|
-
this.logger.error(error);
|
|
380
|
-
throw error;
|
|
381
|
-
}
|
|
544
|
+
super(table, app);
|
|
382
545
|
}
|
|
383
546
|
};
|
|
384
|
-
_ts_decorate([
|
|
385
|
-
(0, __moostjs_event_http.Get)("query"),
|
|
386
|
-
_ts_param(0, (0, __moostjs_event_http.Url)()),
|
|
387
|
-
_ts_metadata("design:type", Function),
|
|
388
|
-
_ts_metadata("design:paramtypes", [String]),
|
|
389
|
-
_ts_metadata("design:returntype", Promise)
|
|
390
|
-
], AsDbController.prototype, "query", null);
|
|
391
|
-
_ts_decorate([
|
|
392
|
-
(0, __moostjs_event_http.Get)("pages"),
|
|
393
|
-
_ts_param(0, (0, __moostjs_event_http.Url)()),
|
|
394
|
-
_ts_metadata("design:type", Function),
|
|
395
|
-
_ts_metadata("design:paramtypes", [String]),
|
|
396
|
-
_ts_metadata("design:returntype", Promise)
|
|
397
|
-
], AsDbController.prototype, "pages", null);
|
|
398
|
-
_ts_decorate([
|
|
399
|
-
(0, __moostjs_event_http.Get)("one/:id"),
|
|
400
|
-
_ts_param(0, (0, moost.Param)("id")),
|
|
401
|
-
_ts_param(1, (0, __moostjs_event_http.Url)()),
|
|
402
|
-
_ts_metadata("design:type", Function),
|
|
403
|
-
_ts_metadata("design:paramtypes", [String, String]),
|
|
404
|
-
_ts_metadata("design:returntype", Promise)
|
|
405
|
-
], AsDbController.prototype, "getOne", null);
|
|
406
547
|
_ts_decorate([
|
|
407
548
|
(0, __moostjs_event_http.Post)(""),
|
|
408
549
|
_ts_param(0, (0, __moostjs_event_http.Body)()),
|
|
@@ -432,13 +573,14 @@ _ts_decorate([
|
|
|
432
573
|
_ts_metadata("design:returntype", Promise)
|
|
433
574
|
], AsDbController.prototype, "remove", null);
|
|
434
575
|
_ts_decorate([
|
|
435
|
-
(0, __moostjs_event_http.
|
|
576
|
+
(0, __moostjs_event_http.Delete)(""),
|
|
577
|
+
_ts_param(0, (0, __moostjs_event_http.Query)()),
|
|
436
578
|
_ts_metadata("design:type", Function),
|
|
437
|
-
_ts_metadata("design:paramtypes", []),
|
|
438
|
-
_ts_metadata("design:returntype",
|
|
439
|
-
], AsDbController.prototype, "
|
|
579
|
+
_ts_metadata("design:paramtypes", [typeof Record === "undefined" ? Object : Record]),
|
|
580
|
+
_ts_metadata("design:returntype", Promise)
|
|
581
|
+
], AsDbController.prototype, "removeComposite", null);
|
|
440
582
|
AsDbController = _ts_decorate([
|
|
441
|
-
|
|
583
|
+
(0, moost.Inherit)(),
|
|
442
584
|
_ts_param(0, (0, moost.Inject)(TABLE_DEF)),
|
|
443
585
|
_ts_metadata("design:type", Function),
|
|
444
586
|
_ts_metadata("design:paramtypes", [typeof AtscriptDbTable === "undefined" ? Object : AtscriptDbTable, typeof moost.Moost === "undefined" ? Object : moost.Moost])
|
|
@@ -451,7 +593,16 @@ Object.defineProperty(exports, 'AsDbController', {
|
|
|
451
593
|
return AsDbController;
|
|
452
594
|
}
|
|
453
595
|
});
|
|
596
|
+
Object.defineProperty(exports, 'AsDbReadableController', {
|
|
597
|
+
enumerable: true,
|
|
598
|
+
get: function () {
|
|
599
|
+
return AsDbReadableController;
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
exports.READABLE_DEF = READABLE_DEF
|
|
603
|
+
exports.ReadableController = ReadableController
|
|
454
604
|
exports.TABLE_DEF = TABLE_DEF
|
|
455
605
|
exports.TableController = TableController
|
|
456
606
|
exports.UseValidationErrorTransform = UseValidationErrorTransform
|
|
607
|
+
exports.ViewController = ViewController
|
|
457
608
|
exports.validationErrorTransform = validationErrorTransform
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as _atscript_typescript_serialize from '@atscript/typescript/serialize';
|
|
2
2
|
import * as _atscript_utils_db from '@atscript/utils-db';
|
|
3
|
-
import {
|
|
3
|
+
import { AtscriptDbReadable, Uniquery, FilterExpr, UniqueryControls, AtscriptDbTable } from '@atscript/utils-db';
|
|
4
4
|
import * as _uniqu_url from '@uniqu/url';
|
|
5
5
|
import { TAtscriptAnnotatedType, TAtscriptDataType, Validator } from '@atscript/typescript/utils';
|
|
6
6
|
import { HttpError } from '@moostjs/event-http';
|
|
@@ -8,27 +8,22 @@ import * as moost from 'moost';
|
|
|
8
8
|
import { TConsoleBase, Moost } from 'moost';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* adapter-specific imports.
|
|
11
|
+
* Read-only database controller for Moost that works with any `AtscriptDbReadable`
|
|
12
|
+
* (tables or views). Provides query, pages, getOne, and meta endpoints.
|
|
14
13
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* @Provide(TABLE_DEF, () => driver.getTable(MyType))
|
|
18
|
-
* @TableController(MyType)
|
|
19
|
-
* export class MyController extends AsDbController<typeof MyType> {}
|
|
20
|
-
* ```
|
|
14
|
+
* For write operations (insert, replace, update, delete), use {@link AsDbController}.
|
|
15
|
+
* For views, use {@link AsDbViewController}.
|
|
21
16
|
*/
|
|
22
|
-
declare class
|
|
23
|
-
/** Reference to the underlying table. */
|
|
24
|
-
protected
|
|
17
|
+
declare class AsDbReadableController<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType, DataType = TAtscriptDataType<T>> {
|
|
18
|
+
/** Reference to the underlying readable (table or view). */
|
|
19
|
+
protected readable: AtscriptDbReadable<T>;
|
|
25
20
|
/** Application-scoped logger. */
|
|
26
21
|
protected logger: TConsoleBase;
|
|
27
22
|
/** Cached serialized type definition (static, computed once). */
|
|
28
23
|
private _serializedType;
|
|
29
24
|
/** Cached search index list (static, computed once). */
|
|
30
25
|
private _searchIndexes;
|
|
31
|
-
constructor(
|
|
26
|
+
constructor(readable: AtscriptDbReadable<T>, app: Moost);
|
|
32
27
|
/**
|
|
33
28
|
* One-time initialization hook. Override to seed data, register watchers, etc.
|
|
34
29
|
*/
|
|
@@ -50,16 +45,13 @@ declare class AsDbController<T extends TAtscriptAnnotatedType = TAtscriptAnnotat
|
|
|
50
45
|
* Transform projection before querying.
|
|
51
46
|
*/
|
|
52
47
|
protected transformProjection(projection?: UniqueryControls['$select']): UniqueryControls['$select'] | undefined;
|
|
53
|
-
/**
|
|
54
|
-
* Intercepts write operations. Return `undefined` to abort.
|
|
55
|
-
*/
|
|
56
|
-
protected onWrite(action: 'insert' | 'insertMany' | 'replace' | 'update', data: unknown): unknown | Promise<unknown | undefined>;
|
|
57
|
-
/**
|
|
58
|
-
* Intercepts delete operations. Return `undefined` to abort.
|
|
59
|
-
*/
|
|
60
|
-
protected onRemove(id: unknown): unknown | Promise<unknown | undefined>;
|
|
61
48
|
protected parseQueryString(url: string): _uniqu_url.UrlQuery;
|
|
62
49
|
protected returnOne(result: Promise<DataType | null>): Promise<DataType | HttpError>;
|
|
50
|
+
/**
|
|
51
|
+
* Extracts a composite identifier object from query params.
|
|
52
|
+
* Tries composite primary key first, then compound unique indexes.
|
|
53
|
+
*/
|
|
54
|
+
protected extractCompositeId(query: Record<string, string>): Record<string, unknown> | HttpError;
|
|
63
55
|
/**
|
|
64
56
|
* **GET /query** — returns an array of records or a count.
|
|
65
57
|
*/
|
|
@@ -78,16 +70,53 @@ declare class AsDbController<T extends TAtscriptAnnotatedType = TAtscriptAnnotat
|
|
|
78
70
|
* **GET /one/:id** — retrieves a single record by ID or unique property.
|
|
79
71
|
*/
|
|
80
72
|
getOne(id: string, url: string): Promise<DataType | HttpError>;
|
|
73
|
+
/**
|
|
74
|
+
* **GET /one?field1=val1&field2=val2** — retrieves a single record by composite key
|
|
75
|
+
* (composite primary key or compound unique index).
|
|
76
|
+
*/
|
|
77
|
+
getOneComposite(query: Record<string, string>, url: string): Promise<DataType | HttpError>;
|
|
78
|
+
/**
|
|
79
|
+
* **GET /meta** — returns table/view metadata for UI.
|
|
80
|
+
*/
|
|
81
|
+
meta(): {
|
|
82
|
+
searchable: boolean;
|
|
83
|
+
searchIndexes: _atscript_utils_db.TSearchIndexInfo[];
|
|
84
|
+
type: _atscript_typescript_serialize.TSerializedAnnotatedType;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Full CRUD database controller for Moost that works with any `AtscriptDbTable` +
|
|
90
|
+
* `BaseDbAdapter`. Extends {@link AsDbReadableController} with write operations.
|
|
91
|
+
*
|
|
92
|
+
* Subclass and provide the table via DI:
|
|
93
|
+
* ```ts
|
|
94
|
+
* @TableController(usersTable)
|
|
95
|
+
* export class UsersController extends AsDbController<typeof UserModel> {}
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
declare class AsDbController<T extends TAtscriptAnnotatedType = TAtscriptAnnotatedType, DataType = TAtscriptDataType<T>> extends AsDbReadableController<T, DataType> {
|
|
99
|
+
/** Reference to the underlying table (typed for write access). */
|
|
100
|
+
protected get table(): AtscriptDbTable<T>;
|
|
101
|
+
constructor(table: AtscriptDbTable<T>, app: Moost);
|
|
102
|
+
/**
|
|
103
|
+
* Intercepts write operations. Return `undefined` to abort.
|
|
104
|
+
*/
|
|
105
|
+
protected onWrite(action: 'insert' | 'insertMany' | 'replace' | 'replaceMany' | 'update' | 'updateMany', data: unknown): unknown | Promise<unknown | undefined>;
|
|
106
|
+
/**
|
|
107
|
+
* Intercepts delete operations. Return `undefined` to abort.
|
|
108
|
+
*/
|
|
109
|
+
protected onRemove(id: unknown): unknown | Promise<unknown | undefined>;
|
|
81
110
|
/**
|
|
82
111
|
* **POST /** — inserts one or many records.
|
|
83
112
|
*/
|
|
84
113
|
insert(payload: unknown): Promise<HttpError | unknown>;
|
|
85
114
|
/**
|
|
86
|
-
* **PUT /** — fully replaces
|
|
115
|
+
* **PUT /** — fully replaces one or many records matched by primary key.
|
|
87
116
|
*/
|
|
88
117
|
replace(payload: unknown): Promise<HttpError | unknown>;
|
|
89
118
|
/**
|
|
90
|
-
* **PATCH /** — partially updates
|
|
119
|
+
* **PATCH /** — partially updates one or many records matched by primary key.
|
|
91
120
|
*/
|
|
92
121
|
update(payload: unknown): Promise<HttpError | unknown>;
|
|
93
122
|
/**
|
|
@@ -95,20 +124,23 @@ declare class AsDbController<T extends TAtscriptAnnotatedType = TAtscriptAnnotat
|
|
|
95
124
|
*/
|
|
96
125
|
remove(id: string): Promise<HttpError | unknown>;
|
|
97
126
|
/**
|
|
98
|
-
* **
|
|
127
|
+
* **DELETE /?field1=val1&field2=val2** — removes a record by composite key
|
|
128
|
+
* (composite primary key or compound unique index).
|
|
99
129
|
*/
|
|
100
|
-
|
|
101
|
-
searchable: boolean;
|
|
102
|
-
searchIndexes: _atscript_utils_db.TSearchIndexInfo[];
|
|
103
|
-
type: _atscript_typescript_serialize.TSerializedAnnotatedType;
|
|
104
|
-
};
|
|
130
|
+
removeComposite(query: Record<string, string>): Promise<HttpError | unknown>;
|
|
105
131
|
}
|
|
106
132
|
|
|
133
|
+
/**
|
|
134
|
+
* DI token under which the {@link AtscriptDbReadable} instance
|
|
135
|
+
* is exposed to the readable controller's constructor via `@Inject`.
|
|
136
|
+
*/
|
|
137
|
+
declare const READABLE_DEF = "__atscript_db_readable_def";
|
|
107
138
|
/**
|
|
108
139
|
* DI token under which the {@link AtscriptDbTable} instance
|
|
109
140
|
* is exposed to the controller's constructor via `@Inject`.
|
|
141
|
+
* Points to the same token as READABLE_DEF for backward compatibility.
|
|
110
142
|
*/
|
|
111
|
-
declare const TABLE_DEF = "
|
|
143
|
+
declare const TABLE_DEF = "__atscript_db_readable_def";
|
|
112
144
|
/**
|
|
113
145
|
* Combines the boilerplate needed to turn an {@link AsDbController}
|
|
114
146
|
* subclass into a fully wired HTTP controller for a given `@db.table` model.
|
|
@@ -130,8 +162,32 @@ declare const TABLE_DEF = "__atscript_db_table_def";
|
|
|
130
162
|
* ```
|
|
131
163
|
*/
|
|
132
164
|
declare const TableController: (table: AtscriptDbTable, prefix?: string) => MethodDecorator & ClassDecorator & ParameterDecorator & PropertyDecorator;
|
|
165
|
+
/**
|
|
166
|
+
* Combines the boilerplate needed to turn an {@link AsDbReadableController}
|
|
167
|
+
* subclass into a fully wired HTTP controller for a given `@db.view` or `@db.table` model.
|
|
168
|
+
*
|
|
169
|
+
* @param readable The {@link AtscriptDbReadable} instance (table or view).
|
|
170
|
+
* @param prefix Optional route prefix. Defaults to `readable.tableName`.
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```ts
|
|
174
|
+
* @ReadableController(activeTasksView)
|
|
175
|
+
* export class ActiveTasksController extends AsDbReadableController<typeof ActiveTasks> {}
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
178
|
+
declare const ReadableController: (readable: AtscriptDbReadable, prefix?: string) => MethodDecorator & ClassDecorator & ParameterDecorator & PropertyDecorator;
|
|
179
|
+
/**
|
|
180
|
+
* Alias for {@link ReadableController} — use with view-backed controllers.
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```ts
|
|
184
|
+
* @ViewController(activeTasksView)
|
|
185
|
+
* export class ActiveTasksController extends AsDbReadableController<typeof ActiveTasks> {}
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
declare const ViewController: (readable: AtscriptDbReadable, prefix?: string) => MethodDecorator & ClassDecorator & ParameterDecorator & PropertyDecorator;
|
|
133
189
|
|
|
134
190
|
declare const validationErrorTransform: () => moost.TInterceptorDef;
|
|
135
191
|
declare const UseValidationErrorTransform: () => ClassDecorator & MethodDecorator;
|
|
136
192
|
|
|
137
|
-
export { AsDbController, TABLE_DEF, TableController, UseValidationErrorTransform, validationErrorTransform };
|
|
193
|
+
export { AsDbController, AsDbReadableController, READABLE_DEF, ReadableController, TABLE_DEF, TableController, UseValidationErrorTransform, ViewController, validationErrorTransform };
|
package/dist/index.mjs
CHANGED
|
@@ -1,20 +1,33 @@
|
|
|
1
1
|
import { ValidatorError, defineAnnotatedType, serializeAnnotatedType, throwFeatureDisabled } from "@atscript/typescript/utils";
|
|
2
|
-
import { Body, Delete, Get, HttpError, Patch, Post, Put, Url } from "@moostjs/event-http";
|
|
2
|
+
import { Body, Delete, Get, HttpError, Patch, Post, Put, Query, Url } from "@moostjs/event-http";
|
|
3
3
|
import { ApplyDecorators, Controller, Inherit, Inject, Intercept, Moost, Param, Provide, TInterceptorPriority, defineInterceptor } from "moost";
|
|
4
4
|
import { parseUrl } from "@uniqu/url";
|
|
5
|
+
import { DbError } from "@atscript/utils-db";
|
|
5
6
|
|
|
6
7
|
//#region packages/moost-db/src/decorators.ts
|
|
7
|
-
const
|
|
8
|
+
const READABLE_DEF = "__atscript_db_readable_def";
|
|
9
|
+
const TABLE_DEF = READABLE_DEF;
|
|
8
10
|
const TableController = (table, prefix) => ApplyDecorators(Provide(TABLE_DEF, () => table), Controller(prefix || table.tableName), Inherit());
|
|
11
|
+
const ReadableController = (readable, prefix) => ApplyDecorators(Provide(READABLE_DEF, () => readable), Controller(prefix || readable.tableName), Inherit());
|
|
12
|
+
const ViewController = ReadableController;
|
|
9
13
|
|
|
10
14
|
//#endregion
|
|
11
15
|
//#region packages/moost-db/src/validation-interceptor.ts
|
|
16
|
+
const dbErrorCodeToStatus = { CONFLICT: 409 };
|
|
12
17
|
function transformValidationError(error, reply) {
|
|
13
18
|
if (error instanceof ValidatorError) reply(new HttpError(400, {
|
|
14
19
|
message: error.message,
|
|
15
20
|
statusCode: 400,
|
|
16
21
|
errors: error.errors
|
|
17
22
|
}));
|
|
23
|
+
else if (error instanceof DbError) {
|
|
24
|
+
const statusCode = dbErrorCodeToStatus[error.code] ?? 400;
|
|
25
|
+
reply(new HttpError(statusCode, {
|
|
26
|
+
message: error.message,
|
|
27
|
+
statusCode,
|
|
28
|
+
errors: error.errors
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
18
31
|
}
|
|
19
32
|
const validationErrorTransform = () => defineInterceptor({ error: transformValidationError }, TInterceptorPriority.CATCH_ERROR);
|
|
20
33
|
const UseValidationErrorTransform = () => Intercept(validationErrorTransform());
|
|
@@ -121,7 +134,7 @@ defineAnnotatedType("object", SortControlDto).propPattern(/./, defineAnnotatedTy
|
|
|
121
134
|
defineAnnotatedType("object", SelectControlDto).propPattern(/./, defineAnnotatedType("union").item(defineAnnotatedType().designType("number").value(1).$type).item(defineAnnotatedType().designType("number").value(0).$type).$type);
|
|
122
135
|
|
|
123
136
|
//#endregion
|
|
124
|
-
//#region packages/moost-db/src/as-db.controller.ts
|
|
137
|
+
//#region packages/moost-db/src/as-db-readable.controller.ts
|
|
125
138
|
function _define_property(obj, key, value) {
|
|
126
139
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
127
140
|
value,
|
|
@@ -132,21 +145,21 @@ function _define_property(obj, key, value) {
|
|
|
132
145
|
else obj[key] = value;
|
|
133
146
|
return obj;
|
|
134
147
|
}
|
|
135
|
-
function _ts_decorate(decorators, target, key, desc) {
|
|
148
|
+
function _ts_decorate$1(decorators, target, key, desc) {
|
|
136
149
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
137
150
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
138
151
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
139
152
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
140
153
|
}
|
|
141
|
-
function _ts_metadata(k, v) {
|
|
154
|
+
function _ts_metadata$1(k, v) {
|
|
142
155
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
143
156
|
}
|
|
144
|
-
function _ts_param(paramIndex, decorator) {
|
|
157
|
+
function _ts_param$1(paramIndex, decorator) {
|
|
145
158
|
return function(target, key) {
|
|
146
159
|
decorator(target, key, paramIndex);
|
|
147
160
|
};
|
|
148
161
|
}
|
|
149
|
-
var
|
|
162
|
+
var AsDbReadableController = class {
|
|
150
163
|
/**
|
|
151
164
|
* One-time initialization hook. Override to seed data, register watchers, etc.
|
|
152
165
|
*/ init() {}
|
|
@@ -168,7 +181,7 @@ var AsDbController = class {
|
|
|
168
181
|
return undefined;
|
|
169
182
|
}
|
|
170
183
|
validateInsights(insights) {
|
|
171
|
-
for (const key of insights.keys()) if (!this.
|
|
184
|
+
for (const key of insights.keys()) if (!this.readable.flatMap.has(key)) return `Unknown field "${key}"`;
|
|
172
185
|
return undefined;
|
|
173
186
|
}
|
|
174
187
|
validateParsed(parsed, type) {
|
|
@@ -178,6 +191,18 @@ var AsDbController = class {
|
|
|
178
191
|
const insightsError = this.validateInsights(parsed.insights);
|
|
179
192
|
if (insightsError) return new HttpError(400, insightsError);
|
|
180
193
|
}
|
|
194
|
+
const withRelations = parsed.controls.$with;
|
|
195
|
+
if (withRelations?.length) {
|
|
196
|
+
const relations = this.readable.relations;
|
|
197
|
+
for (const rel of withRelations) if (!rel.name.includes(".") && !relations.has(rel.name)) return new HttpError(400, {
|
|
198
|
+
message: `Unknown relation "${rel.name}" in $with. Available relations: ${[...relations.keys()].join(", ") || "(none)"}`,
|
|
199
|
+
statusCode: 400,
|
|
200
|
+
errors: [{
|
|
201
|
+
path: "$with",
|
|
202
|
+
message: `Unknown relation "${rel.name}"`
|
|
203
|
+
}]
|
|
204
|
+
});
|
|
205
|
+
}
|
|
181
206
|
return undefined;
|
|
182
207
|
}
|
|
183
208
|
/**
|
|
@@ -190,16 +215,6 @@ var AsDbController = class {
|
|
|
190
215
|
*/ transformProjection(projection) {
|
|
191
216
|
return projection;
|
|
192
217
|
}
|
|
193
|
-
/**
|
|
194
|
-
* Intercepts write operations. Return `undefined` to abort.
|
|
195
|
-
*/ onWrite(action, data) {
|
|
196
|
-
return data;
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Intercepts delete operations. Return `undefined` to abort.
|
|
200
|
-
*/ onRemove(id) {
|
|
201
|
-
return id;
|
|
202
|
-
}
|
|
203
218
|
parseQueryString(url) {
|
|
204
219
|
const idx = url.indexOf("?");
|
|
205
220
|
return parseUrl(idx >= 0 ? url.slice(idx + 1) : "");
|
|
@@ -210,6 +225,38 @@ var AsDbController = class {
|
|
|
210
225
|
return item;
|
|
211
226
|
}
|
|
212
227
|
/**
|
|
228
|
+
* Extracts a composite identifier object from query params.
|
|
229
|
+
* Tries composite primary key first, then compound unique indexes.
|
|
230
|
+
*/ extractCompositeId(query) {
|
|
231
|
+
const pkFields = this.readable.primaryKeys;
|
|
232
|
+
if (pkFields.length > 1) {
|
|
233
|
+
const idObj = {};
|
|
234
|
+
let allPresent = true;
|
|
235
|
+
for (const field of pkFields) {
|
|
236
|
+
if (query[field] === undefined) {
|
|
237
|
+
allPresent = false;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
idObj[field] = query[field];
|
|
241
|
+
}
|
|
242
|
+
if (allPresent) return idObj;
|
|
243
|
+
}
|
|
244
|
+
for (const index of this.readable.indexes.values()) {
|
|
245
|
+
if (index.type !== "unique" || index.fields.length < 2) continue;
|
|
246
|
+
const idObj = {};
|
|
247
|
+
let allPresent = true;
|
|
248
|
+
for (const indexField of index.fields) {
|
|
249
|
+
if (query[indexField.name] === undefined) {
|
|
250
|
+
allPresent = false;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
idObj[indexField.name] = query[indexField.name];
|
|
254
|
+
}
|
|
255
|
+
if (allPresent) return idObj;
|
|
256
|
+
}
|
|
257
|
+
return new HttpError(400, "Query params do not match any composite primary key or compound unique index");
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
213
260
|
* **GET /query** — returns an array of records or a count.
|
|
214
261
|
*/ async query(url) {
|
|
215
262
|
const parsed = this.parseQueryString(url);
|
|
@@ -218,7 +265,7 @@ var AsDbController = class {
|
|
|
218
265
|
const controls = parsed.controls;
|
|
219
266
|
const filter = this.transformFilter(parsed.filter);
|
|
220
267
|
const select = this.transformProjection(controls.$select);
|
|
221
|
-
if (controls.$count) return this.
|
|
268
|
+
if (controls.$count) return this.readable.count({
|
|
222
269
|
filter,
|
|
223
270
|
controls: {
|
|
224
271
|
...controls,
|
|
@@ -227,7 +274,7 @@ var AsDbController = class {
|
|
|
227
274
|
});
|
|
228
275
|
const searchTerm = controls.$search;
|
|
229
276
|
const indexName = controls.$index;
|
|
230
|
-
if (searchTerm && this.
|
|
277
|
+
if (searchTerm && this.readable.isSearchable()) return this.readable.search(searchTerm, {
|
|
231
278
|
filter,
|
|
232
279
|
controls: {
|
|
233
280
|
...controls,
|
|
@@ -235,7 +282,7 @@ var AsDbController = class {
|
|
|
235
282
|
$limit: controls.$limit || 1e3
|
|
236
283
|
}
|
|
237
284
|
}, indexName);
|
|
238
|
-
return this.
|
|
285
|
+
return this.readable.findMany({
|
|
239
286
|
filter,
|
|
240
287
|
controls: {
|
|
241
288
|
...controls,
|
|
@@ -268,8 +315,8 @@ var AsDbController = class {
|
|
|
268
315
|
}
|
|
269
316
|
};
|
|
270
317
|
let result;
|
|
271
|
-
if (searchTerm && this.
|
|
272
|
-
else result = await this.
|
|
318
|
+
if (searchTerm && this.readable.isSearchable()) result = await this.readable.searchWithCount(searchTerm, query, indexName);
|
|
319
|
+
else result = await this.readable.findManyWithCount(query);
|
|
273
320
|
return {
|
|
274
321
|
data: result.data,
|
|
275
322
|
page,
|
|
@@ -286,31 +333,164 @@ else result = await this.table.findManyWithCount(query);
|
|
|
286
333
|
const error = this.validateParsed(parsed, "getOne");
|
|
287
334
|
if (error) return error;
|
|
288
335
|
const select = this.transformProjection(parsed.controls.$select);
|
|
289
|
-
|
|
336
|
+
const controls = {
|
|
337
|
+
...parsed.controls,
|
|
338
|
+
$select: select
|
|
339
|
+
};
|
|
340
|
+
return this.returnOne(this.readable.findById(id, { controls }));
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* **GET /one?field1=val1&field2=val2** — retrieves a single record by composite key
|
|
344
|
+
* (composite primary key or compound unique index).
|
|
345
|
+
*/ async getOneComposite(query, url) {
|
|
346
|
+
const idObj = this.extractCompositeId(query);
|
|
347
|
+
if (idObj instanceof HttpError) return idObj;
|
|
348
|
+
const parsed = this.parseQueryString(url);
|
|
349
|
+
const select = this.transformProjection(parsed.controls.$select);
|
|
350
|
+
const controls = {
|
|
351
|
+
...parsed.controls,
|
|
352
|
+
$select: select
|
|
353
|
+
};
|
|
354
|
+
return this.returnOne(this.readable.findById(idObj, { controls }));
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* **GET /meta** — returns table/view metadata for UI.
|
|
358
|
+
*/ meta() {
|
|
359
|
+
return {
|
|
360
|
+
searchable: this.readable.isSearchable(),
|
|
361
|
+
searchIndexes: this._searchIndexes,
|
|
362
|
+
type: this._serializedType
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
constructor(readable, app) {
|
|
366
|
+
/** Reference to the underlying readable (table or view). */ _define_property(this, "readable", void 0);
|
|
367
|
+
/** Application-scoped logger. */ _define_property(this, "logger", void 0);
|
|
368
|
+
/** Cached serialized type definition (static, computed once). */ _define_property(this, "_serializedType", void 0);
|
|
369
|
+
/** Cached search index list (static, computed once). */ _define_property(this, "_searchIndexes", void 0);
|
|
370
|
+
_define_property(this, "_queryControlsValidator", void 0);
|
|
371
|
+
_define_property(this, "_pagesControlsValidator", void 0);
|
|
372
|
+
_define_property(this, "_getOneControlsValidator", void 0);
|
|
373
|
+
this.readable = readable;
|
|
374
|
+
this._serializedType = serializeAnnotatedType(readable.type);
|
|
375
|
+
this._searchIndexes = readable.getSearchIndexes();
|
|
376
|
+
this.logger = app.getLogger(`db [${readable.tableName}]`);
|
|
377
|
+
this.logger.info(`Initializing ${readable.isView ? "view" : "table"} controller`);
|
|
378
|
+
try {
|
|
379
|
+
const p = this.init();
|
|
380
|
+
if (p instanceof Promise) p.catch((error) => {
|
|
381
|
+
this.logger.error(error);
|
|
382
|
+
});
|
|
383
|
+
} catch (error) {
|
|
384
|
+
this.logger.error(error);
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
_ts_decorate$1([
|
|
390
|
+
Get("query"),
|
|
391
|
+
_ts_param$1(0, Url()),
|
|
392
|
+
_ts_metadata$1("design:type", Function),
|
|
393
|
+
_ts_metadata$1("design:paramtypes", [String]),
|
|
394
|
+
_ts_metadata$1("design:returntype", Promise)
|
|
395
|
+
], AsDbReadableController.prototype, "query", null);
|
|
396
|
+
_ts_decorate$1([
|
|
397
|
+
Get("pages"),
|
|
398
|
+
_ts_param$1(0, Url()),
|
|
399
|
+
_ts_metadata$1("design:type", Function),
|
|
400
|
+
_ts_metadata$1("design:paramtypes", [String]),
|
|
401
|
+
_ts_metadata$1("design:returntype", Promise)
|
|
402
|
+
], AsDbReadableController.prototype, "pages", null);
|
|
403
|
+
_ts_decorate$1([
|
|
404
|
+
Get("one/:id"),
|
|
405
|
+
_ts_param$1(0, Param("id")),
|
|
406
|
+
_ts_param$1(1, Url()),
|
|
407
|
+
_ts_metadata$1("design:type", Function),
|
|
408
|
+
_ts_metadata$1("design:paramtypes", [String, String]),
|
|
409
|
+
_ts_metadata$1("design:returntype", Promise)
|
|
410
|
+
], AsDbReadableController.prototype, "getOne", null);
|
|
411
|
+
_ts_decorate$1([
|
|
412
|
+
Get("one"),
|
|
413
|
+
_ts_param$1(0, Query()),
|
|
414
|
+
_ts_param$1(1, Url()),
|
|
415
|
+
_ts_metadata$1("design:type", Function),
|
|
416
|
+
_ts_metadata$1("design:paramtypes", [typeof Record === "undefined" ? Object : Record, String]),
|
|
417
|
+
_ts_metadata$1("design:returntype", Promise)
|
|
418
|
+
], AsDbReadableController.prototype, "getOneComposite", null);
|
|
419
|
+
_ts_decorate$1([
|
|
420
|
+
Get("meta"),
|
|
421
|
+
_ts_metadata$1("design:type", Function),
|
|
422
|
+
_ts_metadata$1("design:paramtypes", []),
|
|
423
|
+
_ts_metadata$1("design:returntype", void 0)
|
|
424
|
+
], AsDbReadableController.prototype, "meta", null);
|
|
425
|
+
AsDbReadableController = _ts_decorate$1([
|
|
426
|
+
UseValidationErrorTransform(),
|
|
427
|
+
_ts_param$1(0, Inject(READABLE_DEF)),
|
|
428
|
+
_ts_metadata$1("design:type", Function),
|
|
429
|
+
_ts_metadata$1("design:paramtypes", [typeof AtscriptDbReadable === "undefined" ? Object : AtscriptDbReadable, typeof Moost === "undefined" ? Object : Moost])
|
|
430
|
+
], AsDbReadableController);
|
|
431
|
+
|
|
432
|
+
//#endregion
|
|
433
|
+
//#region packages/moost-db/src/as-db.controller.ts
|
|
434
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
435
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
436
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
437
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
438
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
439
|
+
}
|
|
440
|
+
function _ts_metadata(k, v) {
|
|
441
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
442
|
+
}
|
|
443
|
+
function _ts_param(paramIndex, decorator) {
|
|
444
|
+
return function(target, key) {
|
|
445
|
+
decorator(target, key, paramIndex);
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
var AsDbController = class extends AsDbReadableController {
|
|
449
|
+
/** Reference to the underlying table (typed for write access). */ get table() {
|
|
450
|
+
return this.readable;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Intercepts write operations. Return `undefined` to abort.
|
|
454
|
+
*/ onWrite(action, data) {
|
|
455
|
+
return data;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Intercepts delete operations. Return `undefined` to abort.
|
|
459
|
+
*/ onRemove(id) {
|
|
460
|
+
return id;
|
|
290
461
|
}
|
|
291
462
|
/**
|
|
292
463
|
* **POST /** — inserts one or many records.
|
|
293
464
|
*/ async insert(payload) {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const data$1 = await this.onWrite("insert", arr[0]);
|
|
465
|
+
if (Array.isArray(payload)) {
|
|
466
|
+
const data$1 = await this.onWrite("insertMany", payload);
|
|
297
467
|
if (data$1 === undefined) return new HttpError(500, "Not saved");
|
|
298
|
-
return await this.table.
|
|
468
|
+
return await this.table.insertMany(data$1);
|
|
299
469
|
}
|
|
300
|
-
const data = await this.onWrite("
|
|
470
|
+
const data = await this.onWrite("insert", payload);
|
|
301
471
|
if (data === undefined) return new HttpError(500, "Not saved");
|
|
302
|
-
return await this.table.
|
|
472
|
+
return await this.table.insertOne(data);
|
|
303
473
|
}
|
|
304
474
|
/**
|
|
305
|
-
* **PUT /** — fully replaces
|
|
475
|
+
* **PUT /** — fully replaces one or many records matched by primary key.
|
|
306
476
|
*/ async replace(payload) {
|
|
477
|
+
if (Array.isArray(payload)) {
|
|
478
|
+
const data$1 = await this.onWrite("replaceMany", payload);
|
|
479
|
+
if (data$1 === undefined) return new HttpError(500, "Not saved");
|
|
480
|
+
return await this.table.bulkReplace(data$1);
|
|
481
|
+
}
|
|
307
482
|
const data = await this.onWrite("replace", payload);
|
|
308
483
|
if (data === undefined) return new HttpError(500, "Not saved");
|
|
309
484
|
return await this.table.replaceOne(data);
|
|
310
485
|
}
|
|
311
486
|
/**
|
|
312
|
-
* **PATCH /** — partially updates
|
|
487
|
+
* **PATCH /** — partially updates one or many records matched by primary key.
|
|
313
488
|
*/ async update(payload) {
|
|
489
|
+
if (Array.isArray(payload)) {
|
|
490
|
+
const data$1 = await this.onWrite("updateMany", payload);
|
|
491
|
+
if (data$1 === undefined) return new HttpError(500, "Not saved");
|
|
492
|
+
return await this.table.bulkUpdate(data$1);
|
|
493
|
+
}
|
|
314
494
|
const data = await this.onWrite("update", payload);
|
|
315
495
|
if (data === undefined) return new HttpError(500, "Not saved");
|
|
316
496
|
return await this.table.updateOne(data);
|
|
@@ -325,60 +505,21 @@ else result = await this.table.findManyWithCount(query);
|
|
|
325
505
|
return result;
|
|
326
506
|
}
|
|
327
507
|
/**
|
|
328
|
-
* **
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
508
|
+
* **DELETE /?field1=val1&field2=val2** — removes a record by composite key
|
|
509
|
+
* (composite primary key or compound unique index).
|
|
510
|
+
*/ async removeComposite(query) {
|
|
511
|
+
const idObj = this.extractCompositeId(query);
|
|
512
|
+
if (idObj instanceof HttpError) return idObj;
|
|
513
|
+
const resolvedId = await this.onRemove(idObj);
|
|
514
|
+
if (resolvedId === undefined) return new HttpError(500, "Not deleted");
|
|
515
|
+
const result = await this.table.deleteOne(resolvedId);
|
|
516
|
+
if (result.deletedCount < 1) return new HttpError(404);
|
|
517
|
+
return result;
|
|
335
518
|
}
|
|
336
519
|
constructor(table, app) {
|
|
337
|
-
|
|
338
|
-
/** Application-scoped logger. */ _define_property(this, "logger", void 0);
|
|
339
|
-
/** Cached serialized type definition (static, computed once). */ _define_property(this, "_serializedType", void 0);
|
|
340
|
-
/** Cached search index list (static, computed once). */ _define_property(this, "_searchIndexes", void 0);
|
|
341
|
-
_define_property(this, "_queryControlsValidator", void 0);
|
|
342
|
-
_define_property(this, "_pagesControlsValidator", void 0);
|
|
343
|
-
_define_property(this, "_getOneControlsValidator", void 0);
|
|
344
|
-
this.table = table;
|
|
345
|
-
this._serializedType = serializeAnnotatedType(table.type);
|
|
346
|
-
this._searchIndexes = table.getSearchIndexes();
|
|
347
|
-
this.logger = app.getLogger(`db [${table.tableName}]`);
|
|
348
|
-
this.logger.info("Initializing table controller");
|
|
349
|
-
try {
|
|
350
|
-
const p = this.init();
|
|
351
|
-
if (p instanceof Promise) p.catch((error) => {
|
|
352
|
-
this.logger.error(error);
|
|
353
|
-
});
|
|
354
|
-
} catch (error) {
|
|
355
|
-
this.logger.error(error);
|
|
356
|
-
throw error;
|
|
357
|
-
}
|
|
520
|
+
super(table, app);
|
|
358
521
|
}
|
|
359
522
|
};
|
|
360
|
-
_ts_decorate([
|
|
361
|
-
Get("query"),
|
|
362
|
-
_ts_param(0, Url()),
|
|
363
|
-
_ts_metadata("design:type", Function),
|
|
364
|
-
_ts_metadata("design:paramtypes", [String]),
|
|
365
|
-
_ts_metadata("design:returntype", Promise)
|
|
366
|
-
], AsDbController.prototype, "query", null);
|
|
367
|
-
_ts_decorate([
|
|
368
|
-
Get("pages"),
|
|
369
|
-
_ts_param(0, Url()),
|
|
370
|
-
_ts_metadata("design:type", Function),
|
|
371
|
-
_ts_metadata("design:paramtypes", [String]),
|
|
372
|
-
_ts_metadata("design:returntype", Promise)
|
|
373
|
-
], AsDbController.prototype, "pages", null);
|
|
374
|
-
_ts_decorate([
|
|
375
|
-
Get("one/:id"),
|
|
376
|
-
_ts_param(0, Param("id")),
|
|
377
|
-
_ts_param(1, Url()),
|
|
378
|
-
_ts_metadata("design:type", Function),
|
|
379
|
-
_ts_metadata("design:paramtypes", [String, String]),
|
|
380
|
-
_ts_metadata("design:returntype", Promise)
|
|
381
|
-
], AsDbController.prototype, "getOne", null);
|
|
382
523
|
_ts_decorate([
|
|
383
524
|
Post(""),
|
|
384
525
|
_ts_param(0, Body()),
|
|
@@ -408,17 +549,18 @@ _ts_decorate([
|
|
|
408
549
|
_ts_metadata("design:returntype", Promise)
|
|
409
550
|
], AsDbController.prototype, "remove", null);
|
|
410
551
|
_ts_decorate([
|
|
411
|
-
|
|
552
|
+
Delete(""),
|
|
553
|
+
_ts_param(0, Query()),
|
|
412
554
|
_ts_metadata("design:type", Function),
|
|
413
|
-
_ts_metadata("design:paramtypes", []),
|
|
414
|
-
_ts_metadata("design:returntype",
|
|
415
|
-
], AsDbController.prototype, "
|
|
555
|
+
_ts_metadata("design:paramtypes", [typeof Record === "undefined" ? Object : Record]),
|
|
556
|
+
_ts_metadata("design:returntype", Promise)
|
|
557
|
+
], AsDbController.prototype, "removeComposite", null);
|
|
416
558
|
AsDbController = _ts_decorate([
|
|
417
|
-
|
|
559
|
+
Inherit(),
|
|
418
560
|
_ts_param(0, Inject(TABLE_DEF)),
|
|
419
561
|
_ts_metadata("design:type", Function),
|
|
420
562
|
_ts_metadata("design:paramtypes", [typeof AtscriptDbTable === "undefined" ? Object : AtscriptDbTable, typeof Moost === "undefined" ? Object : Moost])
|
|
421
563
|
], AsDbController);
|
|
422
564
|
|
|
423
565
|
//#endregion
|
|
424
|
-
export { AsDbController, TABLE_DEF, TableController, UseValidationErrorTransform, validationErrorTransform };
|
|
566
|
+
export { AsDbController, AsDbReadableController, READABLE_DEF, ReadableController, TABLE_DEF, TableController, UseValidationErrorTransform, ViewController, validationErrorTransform };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/moost-db",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.36",
|
|
4
4
|
"description": "Generic database controller for Moost with Atscript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"annotations",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"bugs": {
|
|
14
14
|
"url": "https://github.com/moostjs/atscript/issues"
|
|
15
15
|
},
|
|
16
|
-
"license": "
|
|
16
|
+
"license": "MIT",
|
|
17
17
|
"author": "Artem Maltsev",
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|
|
@@ -35,20 +35,20 @@
|
|
|
35
35
|
"./package.json": "./package.json"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@uniqu/url": "^0.0.
|
|
38
|
+
"@uniqu/url": "^0.0.6"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@moostjs/event-http": "^0.6.2",
|
|
42
42
|
"moost": "^0.6.2",
|
|
43
43
|
"vitest": "3.2.4",
|
|
44
|
-
"@atscript/core": "^0.1.
|
|
44
|
+
"@atscript/core": "^0.1.36"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@moostjs/event-http": "^0.6.2",
|
|
48
48
|
"moost": "^0.6.2",
|
|
49
|
-
"@uniqu/core": "^0.0.
|
|
50
|
-
"@atscript/utils-db": "^0.1.
|
|
51
|
-
"@atscript/typescript": "^0.1.
|
|
49
|
+
"@uniqu/core": "^0.0.6",
|
|
50
|
+
"@atscript/utils-db": "^0.1.36",
|
|
51
|
+
"@atscript/typescript": "^0.1.36"
|
|
52
52
|
},
|
|
53
53
|
"scripts": {
|
|
54
54
|
"pub": "pnpm publish --access public",
|