@atscript/moost-db 0.1.116 → 0.1.118

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/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
- import { ValidatorError, defineAnnotatedType, serializeAnnotatedType, throwFeatureDisabled } from "@atscript/typescript/utils";
1
+ import { i as resolveDbSpace, n as clearDbSpaces, r as provideDbSpace, t as DEFAULT_DB_SPACE } from "./db-space-registry-C_wft7kl.mjs";
2
+ import { ValidatorError, defineAnnotatedType, isAnnotatedType, serializeAnnotatedType, throwFeatureDisabled } from "@atscript/typescript/utils";
2
3
  import { Body, Delete, Get, HttpError, Patch, Post, Put, Query, Url } from "@moostjs/event-http";
3
- import { ApplyDecorators, Controller, Inherit, Inject, Intercept, Moost, Param, Provide, Resolve, TInterceptorPriority, defineBeforeInterceptor, defineInterceptor, getMoostMate, useControllerContext } from "moost";
4
+ import { ApplyDecorators, Controller, Inherit, Inject, Intercept, Moost, Optional, Param, Provide, Resolve, TInterceptorPriority, defineBeforeInterceptor, defineInterceptor, getMoostMate, useControllerContext } from "moost";
4
5
  import { parseUrl } from "@uniqu/url";
5
6
  import { DbError } from "@atscript/db";
6
7
  import { buildMemoryPredicate, projectRow, sortRows } from "@atscript/db-memory";
@@ -912,57 +913,140 @@ const READABLE_DEF = "__atscript_db_readable_def";
912
913
  * Points to the same token as READABLE_DEF for backward compatibility.
913
914
  */
914
915
  const TABLE_DEF = READABLE_DEF;
916
+ function normalizeOptions(prefixOrOptions) {
917
+ if (typeof prefixOrOptions === "string") return { prefix: prefixOrOptions };
918
+ return prefixOrOptions ?? {};
919
+ }
920
+ /**
921
+ * Builds the shared binding metadata for a decorator invocation: classifies
922
+ * the binding form, computes the static route prefix, and packages a uniform
923
+ * `resolve()` used by both the DI provide factory and the base controller's
924
+ * `super(undefined, app)` fallback.
925
+ */
926
+ function buildBinding(binding, options, decoratorName) {
927
+ if (isAnnotatedType(binding)) {
928
+ const model = binding;
929
+ const space = options.space ?? model.metadata.get("db.space");
930
+ const prefix = options.prefix || model.metadata.get("db.http.path") || model.metadata.get("db.table") || model.metadata.get("db.view") || model.id || "";
931
+ if (!prefix) throw new Error(`[moost-db] @${decoratorName}: cannot derive a route prefix from the model token (no @db.http.path / @db.table / @db.view and no type id). Pass an explicit prefix.`);
932
+ return {
933
+ meta: {
934
+ model,
935
+ resolve: () => resolveDbSpace(space).get(model)
936
+ },
937
+ prefix
938
+ };
939
+ }
940
+ if (typeof binding === "function") {
941
+ if (!options.prefix) throw new Error(`[moost-db] @${decoratorName}: the lazy factory form needs an explicit route prefix (the readable is not created until app.init()). Pass a prefix, or use the model token form which derives it from @db.http.path / @db.table.`);
942
+ return {
943
+ meta: { resolve: binding },
944
+ prefix: options.prefix
945
+ };
946
+ }
947
+ const readable = binding;
948
+ const prefix = options.prefix || readable.type.metadata.get("db.http.path") || readable.tableName;
949
+ return {
950
+ meta: {
951
+ model: readable.type,
952
+ resolve: () => readable
953
+ },
954
+ prefix
955
+ };
956
+ }
957
+ function bindReadableController(binding, prefixOrOptions, decoratorName) {
958
+ const { meta, prefix } = buildBinding(binding, normalizeOptions(prefixOrOptions), decoratorName);
959
+ return ApplyDecorators(getAtscriptDbMate().decorate((classMeta) => {
960
+ classMeta.atscript_db_readable_binding = meta;
961
+ return classMeta;
962
+ }), Provide(READABLE_DEF, () => meta.resolve()), Controller(prefix), Inherit());
963
+ }
915
964
  /**
916
965
  * Combines the boilerplate needed to turn an {@link AsDbController}
917
966
  * subclass into a fully wired HTTP controller for a given `@db.table` model.
918
967
  *
919
968
  * Internally applies three decorators:
920
- * 1. **Provide** — registers the table instance under {@link TABLE_DEF}.
969
+ * 1. **Provide** — registers the readable resolver under {@link TABLE_DEF}.
921
970
  * 2. **Controller** — registers the class as a Moost HTTP controller
922
- * with an optional route prefix. Defaults to `table.tableName`.
971
+ * with an optional route prefix (defaults to `@db.http.path`, then the
972
+ * table name).
923
973
  * 3. **Inherit** — copies metadata (routes, guards, etc.) from the
924
974
  * parent class so they stay active in the derived controller.
925
975
  *
926
- * @param table The {@link AtscriptDbTable} instance for this controller.
927
- * @param prefix Optional route prefix. Defaults to `table.tableName`.
976
+ * All three binding forms are supported (see {@link TReadableBinding}):
928
977
  *
929
- * @example
930
978
  * ```ts
931
- * ‎@TableController(usersTable)
932
- * export class UsersController extends AsDbController<typeof UserModel> {}
979
+ * ‎@TableController(User) // model token (preferred)
980
+ * ‎@TableController(User, { space: "analytics" })
981
+ * ‎@TableController(() => db.getTable(User), "users") // lazy factory
982
+ * ‎@TableController(usersTable) // instance (legacy)
983
+ * export class UsersController extends AsDbController<typeof User> {}
933
984
  * ```
985
+ *
986
+ * Token and factory forms resolve lazily at first controller instantiation
987
+ * (during `app.init()`), so the `DbSpace` does not have to exist when the
988
+ * controller module is imported. For the token form, register the space with
989
+ * `provideDbSpace(db)` before `app.init()`.
990
+ *
991
+ * @param binding Model token, lazy factory, or {@link AtscriptDbTable} instance.
992
+ * @param prefixOrOptions Route prefix string, or {@link TControllerBindingOptions}.
934
993
  */
935
- const TableController = (table, prefix) => {
936
- const resolvedPath = prefix || table.type.metadata.get("db.http.path");
937
- return ApplyDecorators(Provide(TABLE_DEF, () => table), Controller(resolvedPath || table.tableName), Inherit());
938
- };
994
+ const TableController = (binding, prefixOrOptions) => bindReadableController(binding, prefixOrOptions, "TableController");
939
995
  /**
940
996
  * Combines the boilerplate needed to turn an {@link AsDbReadableController}
941
997
  * subclass into a fully wired HTTP controller for a given `@db.view` or `@db.table` model.
942
998
  *
943
- * @param readable The {@link AtscriptDbReadable} instance (table or view).
944
- * @param prefix Optional route prefix. Defaults to `readable.tableName`.
999
+ * Accepts the same three binding forms as {@link TableController}.
1000
+ *
1001
+ * @param binding Model token, lazy factory, or {@link AtscriptDbReadable} instance.
1002
+ * @param prefixOrOptions Route prefix string, or {@link TControllerBindingOptions}.
945
1003
  *
946
1004
  * @example
947
1005
  * ```ts
948
- * ‎@ReadableController(activeTasksView)
1006
+ * ‎@ReadableController(ActiveTasks)
949
1007
  * export class ActiveTasksController extends AsDbReadableController<typeof ActiveTasks> {}
950
1008
  * ```
951
1009
  */
952
- const ReadableController = (readable, prefix) => {
953
- const resolvedPath = prefix || readable.type.metadata.get("db.http.path");
954
- return ApplyDecorators(Provide(READABLE_DEF, () => readable), Controller(resolvedPath || readable.tableName), Inherit());
955
- };
1010
+ const ReadableController = (binding, prefixOrOptions) => bindReadableController(binding, prefixOrOptions, "ReadableController");
956
1011
  /**
957
1012
  * Alias for {@link ReadableController} — use with view-backed controllers.
958
1013
  *
959
1014
  * @example
960
1015
  * ```ts
961
- * ‎@ViewController(activeTasksView)
1016
+ * ‎@ViewController(ActiveTasks)
962
1017
  * export class ActiveTasksController extends AsDbReadableController<typeof ActiveTasks> {}
963
1018
  * ```
964
1019
  */
965
1020
  const ViewController = ReadableController;
1021
+ /**
1022
+ * Finds the readable binding written by `@TableController` /
1023
+ * `@ReadableController` / `@ViewController` on a controller class, walking the
1024
+ * prototype chain so intermediate undecorated classes don't hide the binding
1025
+ * (nearest decorated ancestor wins).
1026
+ */
1027
+ function findReadableBinding(ctor) {
1028
+ const mate = getAtscriptDbMate();
1029
+ let current = ctor;
1030
+ while (typeof current === "function") {
1031
+ const binding = mate.read(current)?.atscript_db_readable_binding;
1032
+ if (binding) return binding;
1033
+ current = Object.getPrototypeOf(current);
1034
+ }
1035
+ }
1036
+ /**
1037
+ * Resolves the readable bound to a controller class via
1038
+ * {@link findReadableBinding}, throwing with wiring guidance when none is
1039
+ * found.
1040
+ *
1041
+ * Used by {@link AsDbReadableController}'s constructor when `readable` is
1042
+ * `undefined` — i.e. a subclass with its own constructor called
1043
+ * `super(undefined, app)` instead of forwarding an injected instance.
1044
+ */
1045
+ function resolveBoundReadable(ctor) {
1046
+ const binding = findReadableBinding(ctor);
1047
+ if (binding) return binding.resolve();
1048
+ throw new Error(`[moost-db] ${ctor?.name || "controller"}: no readable bound. Either pass a table/view to super(...), or decorate the class with @TableController / @ReadableController (model token, lazy factory, or instance form).`);
1049
+ }
966
1050
  //#endregion
967
1051
  //#region src/permissions/crud-controls.ts
968
1052
  /**
@@ -1012,11 +1096,12 @@ let AsDbReadableController = class AsDbReadableController extends AsReadableCont
1012
1096
  /** Paths the adapter vetoes for filtering (e.g. JSON storage on SQL). Symmetric with `/meta` `filterable: false`. */
1013
1097
  _adapterNonFilterable;
1014
1098
  constructor(readable, app) {
1015
- super(readable.type, readable.tableName, app, readable.isView ? "view" : "table");
1016
- this.readable = readable;
1099
+ const resolved = readable ?? resolveBoundReadable(new.target);
1100
+ super(resolved.type, resolved.tableName, app, resolved.isView ? "view" : "table");
1101
+ this.readable = resolved;
1017
1102
  this._adapterNonFilterable = this._collectAdapterNonFilterable();
1018
1103
  this._gates = this._buildGates();
1019
- this._preferredIdSet = new Set(readable.preferredId ?? []);
1104
+ this._preferredIdSet = new Set(resolved.preferredId ?? []);
1020
1105
  this._quantityRefByPath = this._collectQuantityRefs();
1021
1106
  const defaultOverlay = AsReadableController.prototype.applyMetaOverlay;
1022
1107
  this._overlayIsNoOp = this.applyMetaOverlay === defaultOverlay;
@@ -1673,6 +1758,7 @@ __decorate([
1673
1758
  AsDbReadableController = __decorate([
1674
1759
  Inherit(),
1675
1760
  __decorateParam(0, Inject(READABLE_DEF)),
1761
+ __decorateParam(0, Optional()),
1676
1762
  __decorateMetadata("design:paramtypes", [Object, typeof Moost === "undefined" ? Object : Moost])
1677
1763
  ], AsDbReadableController);
1678
1764
  registerAsDbReadableController(AsDbReadableController);
@@ -1870,6 +1956,7 @@ __decorate([
1870
1956
  AsDbController = __decorate([
1871
1957
  Inherit(),
1872
1958
  __decorateParam(0, Inject(TABLE_DEF)),
1959
+ __decorateParam(0, Optional()),
1873
1960
  __decorateMetadata("design:paramtypes", [Object, typeof Moost === "undefined" ? Object : Moost])
1874
1961
  ], AsDbController);
1875
1962
  //#endregion
@@ -2138,6 +2225,44 @@ AsJsonValueHelpController = __decorate([Inherit(), __decorateMetadata("design:pa
2138
2225
  String
2139
2226
  ])], AsJsonValueHelpController);
2140
2227
  //#endregion
2228
+ //#region src/assert-exposed.ts
2229
+ /**
2230
+ * Dev-mode wiring assertion: warns for every model annotated with
2231
+ * `@db.http.path` that has no registered db controller.
2232
+ *
2233
+ * Call AFTER `await app.init()` (controller bindings are collected during
2234
+ * init). Detection reads the binding metadata written by `@TableController` /
2235
+ * `@ReadableController` / `@ViewController`, so it covers the model-token and
2236
+ * instance forms; lazy-factory bindings can't name their model until resolved
2237
+ * and are ignored.
2238
+ *
2239
+ * Returns the list of unexposed models so callers can escalate (e.g. throw in
2240
+ * CI):
2241
+ *
2242
+ * ```ts
2243
+ * await app.init()
2244
+ * const missing = assertExposed(app, atscriptModels)
2245
+ * if (missing.length && process.env.CI) throw new Error("unexposed models")
2246
+ * ```
2247
+ */
2248
+ function assertExposed(app, models, options) {
2249
+ const logger = options?.logger ?? console;
2250
+ const exposed = /* @__PURE__ */ new Set();
2251
+ for (const overview of app.getControllersOverview()) {
2252
+ const model = findReadableBinding(overview.type)?.model;
2253
+ if (model) exposed.add(model);
2254
+ }
2255
+ const missing = [];
2256
+ for (const model of models) {
2257
+ if (!model.metadata.has("db.http.path")) continue;
2258
+ if (exposed.has(model)) continue;
2259
+ missing.push(model);
2260
+ const path = model.metadata.get("db.http.path");
2261
+ logger.warn(`[moost-db] Model "${model.id ?? path}" declares @db.http.path "${path}" but no registered controller is bound to it. Did you forget to register its controller?`);
2262
+ }
2263
+ return missing;
2264
+ }
2265
+ //#endregion
2141
2266
  //#region src/actions/action-disabled-error.ts
2142
2267
  function buildMessage(action, ids) {
2143
2268
  if (ids !== void 0) return `Action "${action}" is disabled for ${ids.length} of the selected rows`;
@@ -2727,4 +2852,4 @@ function InputForm(formType) {
2727
2852
  */
2728
2853
  const perRow = (fn) => (rows) => rows.map(fn);
2729
2854
  //#endregion
2730
- export { ActionDisabledError, AsDbController, AsDbReadableController, AsJsonValueHelpController, AsReadableController, AsValueHelpController, DbAction, DbActionDefault, DbActionID, DbActionIDs, DbActionRow, DbActionRows, DbActions, DbRowActions, DbRowsActions, DbTableActions, InputForm, ONE_CONTROLS, PAGES_CONTROLS, QUERY_CONTROLS, READABLE_DEF, ReadableController, TABLE_DEF, TableController, UseValidationErrorTransform, ViewController, dbActionBodySlot, dbActionInputSlot, discoverActions, getAtscriptDbMate, getControllerFormType, perRow, useDbActionId, useDbActionIds, useDbActionInput, useDbActionRow, useDbActionRows, validationErrorTransform };
2855
+ export { ActionDisabledError, AsDbController, AsDbReadableController, AsJsonValueHelpController, AsReadableController, AsValueHelpController, DEFAULT_DB_SPACE, DbAction, DbActionDefault, DbActionID, DbActionIDs, DbActionRow, DbActionRows, DbActions, DbRowActions, DbRowsActions, DbTableActions, InputForm, ONE_CONTROLS, PAGES_CONTROLS, QUERY_CONTROLS, READABLE_DEF, ReadableController, TABLE_DEF, TableController, UseValidationErrorTransform, ViewController, assertExposed, clearDbSpaces, dbActionBodySlot, dbActionInputSlot, discoverActions, findReadableBinding, getAtscriptDbMate, getControllerFormType, perRow, provideDbSpace, resolveBoundReadable, resolveDbSpace, useDbActionId, useDbActionIds, useDbActionInput, useDbActionRow, useDbActionRows, validationErrorTransform };
@@ -0,0 +1,21 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_db_space_registry = require("./db-space-registry-DqKr5Zdk.cjs");
3
+ let _atscript_db_memory = require("@atscript/db-memory");
4
+ //#region src/testing.ts
5
+ /**
6
+ * Creates an in-memory {@link DbSpace} (via `@atscript/db-memory`), registers
7
+ * it in the ambient registry for token-based controller binding, and
8
+ * pre-creates tables/views for the given models so relations and FK lookups
9
+ * resolve. Returns the space for direct seeding/assertions.
10
+ *
11
+ * No schema sync is needed — the memory adapter has no DDL.
12
+ */
13
+ function provideTestDbSpace(models, options) {
14
+ const space = options?.space ?? (0, _atscript_db_memory.createAdapter)();
15
+ require_db_space_registry.provideDbSpace(space, options?.name);
16
+ for (const model of models ?? []) space.get(model);
17
+ return space;
18
+ }
19
+ //#endregion
20
+ exports.provideTestDbSpace = provideTestDbSpace;
21
+ exports.resetTestDbSpaces = require_db_space_registry.clearDbSpaces;
@@ -0,0 +1,22 @@
1
+ import { n as clearDbSpaces } from "./db-space-registry-CWpYwZ4R.cjs";
2
+ import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
3
+ import { DbSpace } from "@atscript/db";
4
+
5
+ //#region src/testing.d.ts
6
+ interface TTestDbSpaceOptions {
7
+ /** Registry name — defaults to the default space. */
8
+ name?: string;
9
+ /** Use an existing space instead of creating an in-memory one. */
10
+ space?: DbSpace;
11
+ }
12
+ /**
13
+ * Creates an in-memory {@link DbSpace} (via `@atscript/db-memory`), registers
14
+ * it in the ambient registry for token-based controller binding, and
15
+ * pre-creates tables/views for the given models so relations and FK lookups
16
+ * resolve. Returns the space for direct seeding/assertions.
17
+ *
18
+ * No schema sync is needed — the memory adapter has no DDL.
19
+ */
20
+ declare function provideTestDbSpace(models?: readonly TAtscriptAnnotatedType[], options?: TTestDbSpaceOptions): DbSpace;
21
+ //#endregion
22
+ export { TTestDbSpaceOptions, provideTestDbSpace, clearDbSpaces as resetTestDbSpaces };
@@ -0,0 +1,22 @@
1
+ import { n as clearDbSpaces } from "./db-space-registry-CKR6G-hi.mjs";
2
+ import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
3
+ import { DbSpace } from "@atscript/db";
4
+
5
+ //#region src/testing.d.ts
6
+ interface TTestDbSpaceOptions {
7
+ /** Registry name — defaults to the default space. */
8
+ name?: string;
9
+ /** Use an existing space instead of creating an in-memory one. */
10
+ space?: DbSpace;
11
+ }
12
+ /**
13
+ * Creates an in-memory {@link DbSpace} (via `@atscript/db-memory`), registers
14
+ * it in the ambient registry for token-based controller binding, and
15
+ * pre-creates tables/views for the given models so relations and FK lookups
16
+ * resolve. Returns the space for direct seeding/assertions.
17
+ *
18
+ * No schema sync is needed — the memory adapter has no DDL.
19
+ */
20
+ declare function provideTestDbSpace(models?: readonly TAtscriptAnnotatedType[], options?: TTestDbSpaceOptions): DbSpace;
21
+ //#endregion
22
+ export { TTestDbSpaceOptions, provideTestDbSpace, clearDbSpaces as resetTestDbSpaces };
@@ -0,0 +1,19 @@
1
+ import { n as clearDbSpaces, r as provideDbSpace } from "./db-space-registry-C_wft7kl.mjs";
2
+ import { createAdapter } from "@atscript/db-memory";
3
+ //#region src/testing.ts
4
+ /**
5
+ * Creates an in-memory {@link DbSpace} (via `@atscript/db-memory`), registers
6
+ * it in the ambient registry for token-based controller binding, and
7
+ * pre-creates tables/views for the given models so relations and FK lookups
8
+ * resolve. Returns the space for direct seeding/assertions.
9
+ *
10
+ * No schema sync is needed — the memory adapter has no DDL.
11
+ */
12
+ function provideTestDbSpace(models, options) {
13
+ const space = options?.space ?? createAdapter();
14
+ provideDbSpace(space, options?.name);
15
+ for (const model of models ?? []) space.get(model);
16
+ return space;
17
+ }
18
+ //#endregion
19
+ export { provideTestDbSpace, clearDbSpaces as resetTestDbSpaces };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/moost-db",
3
- "version": "0.1.116",
3
+ "version": "0.1.118",
4
4
  "description": "Generic database controller for Moost with Atscript.",
5
5
  "keywords": [
6
6
  "annotations",
@@ -32,6 +32,11 @@
32
32
  "import": "./dist/index.mjs",
33
33
  "require": "./dist/index.cjs"
34
34
  },
35
+ "./testing": {
36
+ "types": "./dist/testing.d.mts",
37
+ "import": "./dist/testing.mjs",
38
+ "require": "./dist/testing.cjs"
39
+ },
35
40
  "./package.json": "./package.json"
36
41
  },
37
42
  "publishConfig": {
@@ -39,27 +44,27 @@
39
44
  },
40
45
  "dependencies": {
41
46
  "@uniqu/url": "^0.1.7",
42
- "@atscript/db-memory": "^0.1.116"
47
+ "@atscript/db-memory": "^0.1.118"
43
48
  },
44
49
  "devDependencies": {
45
- "@atscript/core": "^0.1.83",
46
- "@atscript/typescript": "^0.1.83",
47
- "@moostjs/event-http": "^0.6.31",
50
+ "@atscript/core": "^0.1.84",
51
+ "@atscript/typescript": "^0.1.84",
52
+ "@moostjs/event-http": "^0.6.32",
48
53
  "@uniqu/core": "^0.1.7",
49
54
  "@wooksjs/event-core": "^0.7.20",
50
55
  "@wooksjs/event-http": "^0.7.20",
51
56
  "@wooksjs/http-body": "^0.7.20",
52
- "moost": "^0.6.31",
53
- "unplugin-atscript": "^0.1.83"
57
+ "moost": "^0.6.32",
58
+ "unplugin-atscript": "^0.1.84"
54
59
  },
55
60
  "peerDependencies": {
56
- "@atscript/typescript": "^0.1.83",
57
- "@moostjs/event-http": "^0.6.31",
61
+ "@atscript/typescript": "^0.1.84",
62
+ "@moostjs/event-http": "^0.6.32",
58
63
  "@uniqu/core": "^0.1.7",
59
64
  "@wooksjs/event-core": "^0.7.20",
60
65
  "@wooksjs/http-body": "^0.7.20",
61
- "moost": "^0.6.31",
62
- "@atscript/db": "^0.1.116"
66
+ "moost": "^0.6.32",
67
+ "@atscript/db": "^0.1.118"
63
68
  },
64
69
  "scripts": {
65
70
  "postinstall": "asc -f dts",