@stonyx/orm 0.3.2-beta.16 → 0.3.2-beta.160

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.
Files changed (66) hide show
  1. package/README.md +1409 -11
  2. package/config/environment.js +99 -12
  3. package/dist/access-verdict.d.ts +85 -0
  4. package/dist/access-verdict.js +284 -0
  5. package/dist/commands.js +34 -0
  6. package/dist/dynamodb/connection.d.ts +31 -0
  7. package/dist/dynamodb/connection.js +28 -0
  8. package/dist/dynamodb/dynamodb-db.d.ts +142 -0
  9. package/dist/dynamodb/dynamodb-db.js +596 -0
  10. package/dist/dynamodb/operation-builder.d.ts +76 -0
  11. package/dist/dynamodb/operation-builder.js +116 -0
  12. package/dist/dynamodb/type-map.d.ts +31 -0
  13. package/dist/dynamodb/type-map.js +48 -0
  14. package/dist/hooks.d.ts +15 -1
  15. package/dist/index.d.ts +3 -0
  16. package/dist/index.js +8 -0
  17. package/dist/main.d.ts +116 -0
  18. package/dist/main.js +129 -0
  19. package/dist/manage-record.js +268 -12
  20. package/dist/mysql/connection.d.ts +1 -0
  21. package/dist/mysql/mysql-db.d.ts +8 -0
  22. package/dist/mysql/mysql-db.js +44 -10
  23. package/dist/orm-request.d.ts +274 -3
  24. package/dist/orm-request.js +1259 -65
  25. package/dist/postgres/connection.d.ts +1 -0
  26. package/dist/postgres/connection.js +8 -6
  27. package/dist/postgres/postgres-db.d.ts +8 -0
  28. package/dist/postgres/postgres-db.js +44 -10
  29. package/dist/record.d.ts +16 -0
  30. package/dist/record.js +154 -6
  31. package/dist/relationships.js +1 -1
  32. package/dist/serializer.js +38 -2
  33. package/dist/setup-rest-server.js +51 -5
  34. package/dist/standalone-db.js +17 -5
  35. package/dist/store.d.ts +13 -1
  36. package/dist/store.js +65 -6
  37. package/dist/types/orm-types.d.ts +260 -0
  38. package/dist/utils.d.ts +44 -0
  39. package/dist/utils.js +47 -0
  40. package/package.json +16 -7
  41. package/src/access-verdict.ts +312 -0
  42. package/src/commands.ts +43 -0
  43. package/src/dynamodb/connection.ts +50 -0
  44. package/src/dynamodb/dynamodb-db.ts +811 -0
  45. package/src/dynamodb/operation-builder.ts +202 -0
  46. package/src/dynamodb/type-map.ts +54 -0
  47. package/src/hooks.ts +15 -1
  48. package/src/index.ts +10 -0
  49. package/src/main.ts +133 -0
  50. package/src/manage-record.ts +294 -18
  51. package/src/mysql/connection.ts +1 -0
  52. package/src/mysql/mysql-db.ts +44 -12
  53. package/src/orm-request.ts +1281 -67
  54. package/src/postgres/connection.ts +10 -6
  55. package/src/postgres/postgres-db.ts +44 -12
  56. package/src/record.ts +182 -6
  57. package/src/relationships.ts +1 -1
  58. package/src/serializer.ts +39 -2
  59. package/src/setup-rest-server.ts +59 -6
  60. package/src/standalone-db.ts +17 -6
  61. package/src/store.ts +68 -6
  62. package/src/types/orm-types.ts +268 -1
  63. package/src/types/stonyx-rest-server.d.ts +14 -1
  64. package/src/types/stonyx.d.ts +7 -1
  65. package/src/utils.ts +50 -0
  66. package/config/environment.ts +0 -91
@@ -0,0 +1,202 @@
1
+ /**
2
+ * DynamoDB operation parameter builders.
3
+ *
4
+ * Each function returns a plain-object "params" bag that can be passed
5
+ * directly to the corresponding DocumentClient command
6
+ * (PutCommand, GetCommand, UpdateCommand, DeleteCommand, ScanCommand, QueryCommand).
7
+ *
8
+ * All functions are pure — no SDK imports here; the caller wraps params
9
+ * in the appropriate Command class.
10
+ */
11
+
12
+ export interface PutItemParams {
13
+ TableName: string;
14
+ Item: Record<string, unknown>;
15
+ ConditionExpression?: string;
16
+ }
17
+
18
+ export interface GetItemParams {
19
+ TableName: string;
20
+ Key: Record<string, unknown>;
21
+ }
22
+
23
+ export interface UpdateItemParams {
24
+ TableName: string;
25
+ Key: Record<string, unknown>;
26
+ UpdateExpression: string;
27
+ ExpressionAttributeNames: Record<string, string>;
28
+ ExpressionAttributeValues: Record<string, unknown>;
29
+ ReturnValues: string;
30
+ }
31
+
32
+ export interface DeleteItemParams {
33
+ TableName: string;
34
+ Key: Record<string, unknown>;
35
+ }
36
+
37
+ export interface ScanParams {
38
+ TableName: string;
39
+ FilterExpression?: string;
40
+ ExpressionAttributeNames?: Record<string, string>;
41
+ ExpressionAttributeValues?: Record<string, unknown>;
42
+ ExclusiveStartKey?: Record<string, unknown>;
43
+ }
44
+
45
+ export interface QueryParams {
46
+ TableName: string;
47
+ IndexName: string;
48
+ KeyConditionExpression: string;
49
+ ExpressionAttributeNames: Record<string, string>;
50
+ ExpressionAttributeValues: Record<string, unknown>;
51
+ ExclusiveStartKey?: Record<string, unknown>;
52
+ }
53
+
54
+ /**
55
+ * PutItem — optionally with a condition expression.
56
+ *
57
+ * Pass conditionExpression = 'attribute_not_exists(id)' to enforce uniqueness.
58
+ */
59
+ export function buildPutItem(
60
+ tableName: string,
61
+ item: Record<string, unknown>,
62
+ conditionExpression?: string,
63
+ ): PutItemParams {
64
+ const params: PutItemParams = { TableName: tableName, Item: item };
65
+ if (conditionExpression) params.ConditionExpression = conditionExpression;
66
+ return params;
67
+ }
68
+
69
+ /**
70
+ * GetItem by primary key.
71
+ */
72
+ export function buildGetItem(
73
+ tableName: string,
74
+ key: Record<string, unknown>,
75
+ ): GetItemParams {
76
+ return { TableName: tableName, Key: key };
77
+ }
78
+
79
+ /**
80
+ * UpdateItem with a SET expression built from the `updates` object.
81
+ * Only the supplied attributes are updated (diff-based call site).
82
+ */
83
+ export function buildUpdateItem(
84
+ tableName: string,
85
+ key: Record<string, unknown>,
86
+ updates: Record<string, unknown>,
87
+ ): UpdateItemParams {
88
+ const names: Record<string, string> = {};
89
+ const values: Record<string, unknown> = {};
90
+ const setClauses: string[] = [];
91
+
92
+ for (const [attr, val] of Object.entries(updates)) {
93
+ const nameAlias = `#${attr}`;
94
+ const valAlias = `:${attr}`;
95
+ names[nameAlias] = attr;
96
+ values[valAlias] = val;
97
+ setClauses.push(`${nameAlias} = ${valAlias}`);
98
+ }
99
+
100
+ return {
101
+ TableName: tableName,
102
+ Key: key,
103
+ UpdateExpression: `SET ${setClauses.join(', ')}`,
104
+ ExpressionAttributeNames: names,
105
+ ExpressionAttributeValues: values,
106
+ ReturnValues: 'NONE',
107
+ };
108
+ }
109
+
110
+ /**
111
+ * DeleteItem by primary key.
112
+ */
113
+ export function buildDeleteItem(
114
+ tableName: string,
115
+ key: Record<string, unknown>,
116
+ ): DeleteItemParams {
117
+ return { TableName: tableName, Key: key };
118
+ }
119
+
120
+ /**
121
+ * ScanCommand params.
122
+ * If conditions are supplied they are rendered as a FilterExpression using AND.
123
+ */
124
+ export function buildScan(
125
+ tableName: string,
126
+ conditions?: Record<string, unknown>,
127
+ exclusiveStartKey?: Record<string, unknown>,
128
+ ): ScanParams {
129
+ const params: ScanParams = { TableName: tableName };
130
+
131
+ if (exclusiveStartKey) params.ExclusiveStartKey = exclusiveStartKey;
132
+
133
+ if (conditions && Object.keys(conditions).length > 0) {
134
+ const validEntries = Object.entries(conditions).filter(
135
+ ([, val]) => val !== undefined && val !== null,
136
+ );
137
+
138
+ if (validEntries.length > 0) {
139
+ const names: Record<string, string> = {};
140
+ const values: Record<string, unknown> = {};
141
+ const clauses: string[] = [];
142
+
143
+ for (const [attr, val] of validEntries) {
144
+ const nameAlias = `#${attr}`;
145
+ const valAlias = `:${attr}`;
146
+ names[nameAlias] = attr;
147
+ values[valAlias] = val;
148
+ clauses.push(`${nameAlias} = ${valAlias}`);
149
+ }
150
+
151
+ params.FilterExpression = clauses.join(' AND ');
152
+ params.ExpressionAttributeNames = names;
153
+ params.ExpressionAttributeValues = values;
154
+ }
155
+ }
156
+
157
+ return params;
158
+ }
159
+
160
+ /**
161
+ * QueryCommand params for a GSI.
162
+ * keyConditions must be in the form { attrName: value } and will be rendered
163
+ * as equality expressions joined by AND.
164
+ */
165
+ export function buildQuery(
166
+ tableName: string,
167
+ indexName: string,
168
+ keyConditions: Record<string, unknown>,
169
+ exclusiveStartKey?: Record<string, unknown>,
170
+ ): QueryParams {
171
+ const validEntries = Object.entries(keyConditions).filter(
172
+ ([, val]) => val !== undefined && val !== null,
173
+ );
174
+
175
+ if (validEntries.length === 0) {
176
+ throw new Error('buildQuery: all keyCondition values are undefined/null');
177
+ }
178
+
179
+ const names: Record<string, string> = {};
180
+ const values: Record<string, unknown> = {};
181
+ const clauses: string[] = [];
182
+
183
+ for (const [attr, val] of validEntries) {
184
+ const nameAlias = `#${attr}`;
185
+ const valAlias = `:${attr}`;
186
+ names[nameAlias] = attr;
187
+ values[valAlias] = val;
188
+ clauses.push(`${nameAlias} = ${valAlias}`);
189
+ }
190
+
191
+ const params: QueryParams = {
192
+ TableName: tableName,
193
+ IndexName: indexName,
194
+ KeyConditionExpression: clauses.join(' AND '),
195
+ ExpressionAttributeNames: names,
196
+ ExpressionAttributeValues: values,
197
+ };
198
+
199
+ if (exclusiveStartKey) params.ExclusiveStartKey = exclusiveStartKey;
200
+
201
+ return params;
202
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Maps ORM attribute types to DynamoDB scalar attribute types.
3
+ * DynamoDB DocumentClient auto-marshalls JS objects, so most values
4
+ * are sent as their native JS types. This map is used by the
5
+ * schema-introspector and startup provisioner for table/GSI creation.
6
+ */
7
+
8
+ export type DynamoScalarType = 'S' | 'N' | 'BOOL';
9
+
10
+ /**
11
+ * DynamoDB attribute-type string for a given ORM attr type.
12
+ * - string → S
13
+ * - number / float → N (stored as Number; DocumentClient handles it)
14
+ * - boolean → BOOL
15
+ * - date → S (ISO-8601 string — enables range queries)
16
+ * - timestamp → N (milliseconds since epoch)
17
+ * - passthrough/trim/etc → S (safe default)
18
+ *
19
+ * For key schema declarations only `S` and `N` are valid; BOOL
20
+ * is legal for attributes but never for a PK/SK.
21
+ */
22
+ const typeMap: Record<string, DynamoScalarType> = {
23
+ string: 'S',
24
+ number: 'N',
25
+ float: 'N',
26
+ boolean: 'BOOL',
27
+ date: 'S',
28
+ timestamp: 'N',
29
+ passthrough: 'S',
30
+ trim: 'S',
31
+ uppercase: 'S',
32
+ ceil: 'N',
33
+ floor: 'N',
34
+ round: 'N',
35
+ };
36
+
37
+ /**
38
+ * Returns the DynamoDB attribute type for a given ORM type string.
39
+ * Defaults to 'S' for any unknown/custom type.
40
+ */
41
+ export function getDynamoType(attrType: string): DynamoScalarType {
42
+ return typeMap[attrType] ?? 'S';
43
+ }
44
+
45
+ /**
46
+ * Returns the DynamoDB key type ('S' | 'N') for use in KeySchema.
47
+ * BOOL cannot be a key attribute; anything that maps to BOOL falls back to 'S'.
48
+ */
49
+ export function getDynamoKeyType(attrType: string): 'S' | 'N' {
50
+ const t = getDynamoType(attrType);
51
+ return t === 'N' ? 'N' : 'S';
52
+ }
53
+
54
+ export default typeMap;
package/src/hooks.ts CHANGED
@@ -37,7 +37,21 @@ export interface HookContext {
37
37
  state?: Record<string, unknown>;
38
38
  /** Previous record state (available in update hooks). */
39
39
  oldState?: unknown;
40
- /** Target record ID for single-record operations. */
40
+ /**
41
+ * Target record ID for single-record operations.
42
+ *
43
+ * SET ONLY UNDER `delete`. `_withHooks` assigns this key in the two
44
+ * `operation === 'delete'` branches and nowhere else, so on `get`, `list`,
45
+ * `create` and `update` the key is ABSENT -- not `undefined`-valued, absent.
46
+ * A hook rule written as `ctx.recordId === '<id>'` never fires on an update;
47
+ * the addressed id is in `ctx.params`. Tracked as abofs/stonyx-orm#242.
48
+ *
49
+ * @see AccessContext.recordId in ./types/orm-types.ts -- an identically-named
50
+ * key on an identically-shaped context object, and NOT interchangeable with
51
+ * this one: it is present on every route `auth()` classifies, and spells
52
+ * absence as `null` rather than `undefined`. They differ in coverage on four
53
+ * of five operations, not only in the absence spelling.
54
+ */
41
55
  recordId?: string | number;
42
56
  /** Response data (available in after hooks). */
43
57
  response?: unknown;
package/src/index.ts CHANGED
@@ -27,6 +27,16 @@ import { count, avg, sum, min, max } from './aggregates.js';
27
27
  export { default } from './main.js';
28
28
  export { store, relationships } from './main.js';
29
29
  export type { PersistErrorDetail } from './main.js';
30
+ export type { AccessContext, AccessFunction, AccessMethod, AccessOperation } from './types/orm-types.js'; // access() contract (#202)
31
+ export type { LinkageFilter } from './types/orm-types.js'; // linkage verdict contract (#234)
32
+ // The request-scoped linkage-verdict factory (#234). PUBLIC on purpose: the
33
+ // README tells a consumer serializing a `Record` outside the REST layer to pass
34
+ // their own resolved `linkage` option, and without an exported factory the only
35
+ // way to follow that advice is to write a SECOND reading of `access()` in
36
+ // consumer code -- the exact "unreviewed second authorization vocabulary" that
37
+ // src/access-verdict.ts exists to prevent, reproduced where no reviewer sees it
38
+ // drift. Give them the one interpreter instead of an invitation to fork it.
39
+ export { createLinkageFilter } from './access-verdict.js';
30
40
  export { Model, View, Serializer }; // base classes
31
41
  export { attr, belongsTo, hasMany, createRecord, updateRecord }; // helpers
32
42
  export { count, avg, sum, min, max }; // aggregate helpers
package/src/main.ts CHANGED
@@ -25,6 +25,7 @@ import baseTransforms from './transforms.js';
25
25
  import Store from './store.js';
26
26
  import Serializer from './serializer.js';
27
27
  import { setup } from '@stonyx/events';
28
+ import type { AccessFunction } from './types/orm-types.js';
28
29
 
29
30
  interface OrmOptions {
30
31
  dbType?: string;
@@ -68,6 +69,53 @@ export default class Orm {
68
69
  views: Record<string, unknown> = {};
69
70
  transforms: Record<string, (value: unknown) => unknown> = { ...baseTransforms };
70
71
  warnings: Set<string> = new Set();
72
+
73
+ /**
74
+ * Model name -> the `access` predicate of the access class that CLAIMS that
75
+ * model (abofs/stonyx-orm#202).
76
+ *
77
+ * Not "that model's own predicate". One access class may claim many models
78
+ * -- `GlobalAccess` in this repo's fixtures declares five, and `models = '*'`
79
+ * claims every model in the store -- and it declares ONE `access` method, so
80
+ * the same function object is registered under every one of those keys.
81
+ * `getAccess('owner') === getAccess('animal')` is `true` there. The
82
+ * one-to-one guarantee below is key -> function, never function -> model,
83
+ * and a caller must not read a resolved predicate as being animal-specific.
84
+ * What makes the ANSWER model-specific is the context the caller passes and
85
+ * the predicate actually reading it -- see {@link Orm#getAccess}.
86
+ *
87
+ * NAMED FOR WHAT IT HOLDS. It was `accessFiles` through review, inherited
88
+ * from the function-local in `setup-rest-server.ts` where the values came
89
+ * straight out of `forEachFileImport` and "files" was defensible. The values
90
+ * are `AccessFunction`s, and the sibling public registries on this class
91
+ * (`models`, `serializers`, `views`, `transforms`) are all plural nouns of
92
+ * the thing held. Renamed here because #202 is the last moment it is free.
93
+ *
94
+ * Populated by `setup-rest-server.ts` at boot, from the access classes under
95
+ * `config.orm.paths.access`, BEFORE any route is mounted -- so it is complete
96
+ * and reachable before the first request can be served. The mapping is
97
+ * one-to-one by construction: setup-rest-server throws if two access classes
98
+ * claim the same model.
99
+ *
100
+ * Keys are model names as declared and stored (kebab-case, e.g.
101
+ * `'phone-number'`), NOT pluralised or mount-prefixed route names.
102
+ *
103
+ * WHY THIS EXISTS AS A FIELD. It used to be a function-local in
104
+ * setup-rest-server that was discarded when that function returned, so at
105
+ * request time there was no way to get from a model name to that model's
106
+ * predicate at all. Each `OrmRequest` held only its OWN model's predicate.
107
+ * That made cross-model authorization -- asking model X's predicate about a
108
+ * request routed to model Y -- inexpressible, which is the capability
109
+ * abofs/stonyx-orm#196 and abofs/stonyx-orm#207 are built on.
110
+ *
111
+ * Empty when the REST server is disabled, and PARTIAL when one access file
112
+ * failed to load (`setup-rest-server.ts` catches, warns and assigns whatever
113
+ * it had). So a missing key does NOT mean the model has no access class.
114
+ * Prefer {@link Orm#getAccess} over indexing this directly -- it is guarded
115
+ * against the prototype chain and this is not.
116
+ */
117
+ accessFunctions: Record<string, AccessFunction> = {};
118
+
71
119
  options!: OrmOptions;
72
120
  sqlDb?: SqlDb;
73
121
  db?: OrmDB | SqlDb;
@@ -90,6 +138,11 @@ export default class Orm {
90
138
  }
91
139
 
92
140
  async init(): Promise<void> {
141
+ // Self-register so log.db works even when @stonyx/orm is in the
142
+ // consumer's `dependencies` (stonyx loader only merges devDependencies).
143
+ const { logColor = 'white', logMethod = 'db' } = config.orm;
144
+ log.defineType(logMethod, logColor);
145
+
93
146
  const { paths, restServer } = config.orm;
94
147
 
95
148
  const promises: Promise<unknown>[] = ['Model', 'Serializer', 'Transform'].map(type => {
@@ -159,6 +212,11 @@ export default class Orm {
159
212
  this.sqlDb = new MysqlDB() as SqlDb;
160
213
  this.db = this.sqlDb;
161
214
  promises.push(this.sqlDb.init());
215
+ } else if (config.orm.dynamodb) {
216
+ const { default: DynamoDBDB } = await import('./dynamodb/dynamodb-db.js');
217
+ this.sqlDb = new DynamoDBDB() as SqlDb;
218
+ this.db = this.sqlDb;
219
+ promises.push(this.sqlDb.init());
162
220
  } else if (this.options.dbType !== 'none') {
163
221
  const db = new DB();
164
222
  this.db = db;
@@ -185,6 +243,81 @@ export default class Orm {
185
243
  Orm.initialized = true;
186
244
  }
187
245
 
246
+ /**
247
+ * Resolve the `access` predicate registered for a model name
248
+ * (abofs/stonyx-orm#202).
249
+ *
250
+ * This is the supported way to reach another model's predicate while
251
+ * servicing a request routed to a different model. Call it with the model
252
+ * name and invoke the result with the live request and an explicit context
253
+ * naming THAT model:
254
+ *
255
+ * ```js
256
+ * const predicate = Orm.instance.getAccess('animal');
257
+ * if (!predicate) return deny;
258
+ * const verdict = predicate(request, { model: 'animal', operation: 'read' });
259
+ * ```
260
+ *
261
+ * WHAT IT RESOLVES. The predicate of the access CLASS that claims the model,
262
+ * which is not necessarily specific to it: one class may claim many models
263
+ * and declares one `access` method, so
264
+ * `getAccess('owner') === getAccess('animal')` is `true` against this repo's
265
+ * fixture. See {@link Orm#accessFunctions}.
266
+ *
267
+ * `undefined` means NO PREDICATE COULD BE RESOLVED for that name. That
268
+ * includes a model whose access class failed to LOAD -- `setup-rest-server`
269
+ * catches, warns and publishes the partial map -- so it is not the same claim
270
+ * as "this model is unrestricted". Treat it as DENY, the same way
271
+ * `AccessContext.operation === undefined` is treated.
272
+ *
273
+ * PASSING THE CONTEXT MAKES A MODEL-CORRECT ANSWER POSSIBLE. It does not, on
274
+ * its own, make the answer model-correct: the resolved predicate has to READ
275
+ * the context. Measured against this repo's shipped access class on a request
276
+ * express dispatched to `GET /owners/angela`, asked about ANIMALS:
277
+ *
278
+ * ```
279
+ * getAccess('animal')(ownersRequest, { model: 'animal', operation: 'read' })
280
+ * -> record => record.id !== 'angela' && record.id !== 'restricted'
281
+ * ```
282
+ *
283
+ * The OWNERS filter, which returns `true` for animal 21 -- the record hidden
284
+ * on every animal surface. Under a mount that predicate recognises neither
285
+ * way it falls through to `['read', 'create', 'update', 'delete']`, a full
286
+ * CRUD grant. Either way: context supplied, answer not the animal answer,
287
+ * wrong in the GRANTING direction, because that predicate is arity-1 and
288
+ * identifies its collection from the request. AC9 asserts the first case on a
289
+ * live dispatch.
290
+ *
291
+ * Every predicate in this repo and in every consumer tree is arity-1 today,
292
+ * and there is no supported way for the caller to tell which kind it got; the
293
+ * boot-time arity warning that would surface it is abofs/stonyx-orm#213. Pass
294
+ * the context, and do not treat a resolved predicate's answer as
295
+ * model-specific until that predicate reads it.
296
+ *
297
+ * OWN PROPERTIES ONLY. A bare `this.accessFunctions[modelName]` walks the
298
+ * prototype chain, so `getAccess('constructor')` resolved `Object` and
299
+ * `getAccess('toString')` resolved `Object.prototype.toString` -- both
300
+ * callable, and the documented `predicate?.(request, ctx)` pattern then
301
+ * returned a TRUTHY value (`Object(request)` is the request), bypassing the
302
+ * `undefined`-means-deny contract entirely. Nothing in the ORM calls
303
+ * `getAccess` yet, so it was not exploitable as shipped -- but #207 takes the
304
+ * model name from the REQUEST BODY (`data.relationships.<key>.data.type`),
305
+ * which would have made a one-field body an authorization bypass. Guarded
306
+ * here at the read point rather than by constructing the map with a null
307
+ * prototype, because the field is public and reassignable and the guard has
308
+ * to hold whatever object it is holding.
309
+ *
310
+ * @param modelName - Model name as declared and stored (kebab-case).
311
+ * @returns The predicate, or `undefined` when no predicate could be resolved
312
+ * for that name. `undefined` is NOT "this model is unrestricted" -- see the
313
+ * note above. Treat it as deny.
314
+ */
315
+ getAccess(modelName: string): AccessFunction | undefined {
316
+ if (!Object.hasOwn(this.accessFunctions, modelName)) return undefined;
317
+
318
+ return this.accessFunctions[modelName];
319
+ }
320
+
188
321
  async startup(): Promise<void> {
189
322
  if (this.sqlDb) await this.sqlDb.startup();
190
323
  }