json-rest-api 2.0.0 → 2.2.0
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/docs/API.md +80 -0
- package/docs/GUIDE/17-row-policies.md +1 -0
- package/docs/GUIDE/21-schema-and-migrations.md +154 -14
- package/docs/GUIDE/30-backend-capabilities.md +8 -5
- package/docs/GUIDE/33-migrating-to-v2.md +27 -7
- package/index.d.ts +1 -0
- package/index.js +2 -0
- package/lib/knex-transaction.js +10 -4
- package/package.json +1 -1
- package/plugins/core/lib/anyapi/anyapi-registry.js +569 -408
- package/plugins/core/lib/anyapi/schema-utils.js +101 -51
- package/plugins/core/lib/anyapi/stored-definition.js +112 -0
- package/plugins/core/lib/querying/aggregate-query.js +170 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +17 -0
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +16 -0
- package/plugins/core/lib/querying-writing/default-data-helpers.js +4 -0
- package/plugins/core/lib/querying-writing/field-types.js +43 -0
- package/plugins/core/lib/querying-writing/request-contracts.js +1 -1
- package/plugins/core/lib/storage/canonical-data-helpers.js +29 -0
- package/plugins/core/lib/storage/ordinary-data-helpers.js +25 -0
- package/plugins/core/lib/storage/storage-types.d.ts +21 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +331 -54
- package/plugins/core/rest-api-plugin-methods/aggregate.js +41 -0
- package/plugins/core/rest-api-plugin-methods/common.js +16 -1
- package/plugins/core/rest-api-plugin.js +2 -0
- package/types/anyapi.d.ts +32 -0
- package/types/plugin-options.d.ts +12 -0
- package/types/resource-methods.d.ts +19 -0
- package/types/resource-schema.d.ts +5 -3
package/docs/API.md
CHANGED
|
@@ -78,6 +78,7 @@ a separate context for each enlisted operation. See [transaction support](#trans
|
|
|
78
78
|
- [PUT - Replace Resource](#put---replace-resource)
|
|
79
79
|
- [PATCH - Update Resource](#patch---update-resource)
|
|
80
80
|
- [DELETE - Remove Resource](#delete---remove-resource)
|
|
81
|
+
- [AGGREGATE - Grouped Totals](#aggregate---grouped-totals)
|
|
81
82
|
3. [Relationship Methods](#relationship-methods)
|
|
82
83
|
- [getRelated - Retrieve Related Resources](#getrelated---retrieve-related-resources)
|
|
83
84
|
- [getRelationship - Retrieve Relationship Identifiers](#getrelationship---retrieve-relationship-identifiers)
|
|
@@ -1136,6 +1137,68 @@ savepoints cannot own library writes.
|
|
|
1136
1137
|
|
|
1137
1138
|
---
|
|
1138
1139
|
|
|
1140
|
+
### AGGREGATE - Grouped Totals
|
|
1141
|
+
|
|
1142
|
+
Groups the records a collection query would select and computes measures for
|
|
1143
|
+
each group, on ordinary and AnyAPI storage alike.
|
|
1144
|
+
|
|
1145
|
+
#### Method Signature
|
|
1146
|
+
|
|
1147
|
+
```javascript
|
|
1148
|
+
await api.resources.sales.aggregate(params, context)
|
|
1149
|
+
```
|
|
1150
|
+
|
|
1151
|
+
#### Parameters
|
|
1152
|
+
|
|
1153
|
+
| Parameter | Type | Description |
|
|
1154
|
+
|-----------|------|-------------|
|
|
1155
|
+
| `groupBy` | string[] | Fields to group by. Omit for one row over all selected records. |
|
|
1156
|
+
| `measures` | object | Output names mapped to `{ count \| countDistinct \| sum \| avg \| min \| max: field }`. Only `count` accepts `'*'`. |
|
|
1157
|
+
| `queryParams.filters` | object | Search schema filters, validated as for `query`. |
|
|
1158
|
+
| `queryParams.sort` | string[] | Grouped field or measure names, `-` for descending. Grouped fields break ties. |
|
|
1159
|
+
| `queryParams.page` | object | `size` and `number`, for grouped results only. `size` defaults to `queryDefaultLimit` and cannot exceed `queryMaxLimit`. |
|
|
1160
|
+
| `transaction` | Knex transaction | Optional transaction to read in. |
|
|
1161
|
+
|
|
1162
|
+
`sum` and `avg` take numeric fields; `min` and `max` also take strings, dates and
|
|
1163
|
+
times. Grouping and counting accept any scalar stored field, including belongs-to
|
|
1164
|
+
foreign keys. Fields that are `hidden`, virtual, computed, or that have a getter or
|
|
1165
|
+
`storage.serialize` are rejected, because SQL would group or sum values other than
|
|
1166
|
+
the ones clients read. `normallyHidden` fields are allowed.
|
|
1167
|
+
|
|
1168
|
+
#### Return Value
|
|
1169
|
+
|
|
1170
|
+
```javascript
|
|
1171
|
+
{
|
|
1172
|
+
data: [{ salesperson: 'Ann', total: 150.5, count: 2 }],
|
|
1173
|
+
meta: { page: { size: 20, number: 1, hasMore: false } } // grouped results only
|
|
1174
|
+
}
|
|
1175
|
+
```
|
|
1176
|
+
|
|
1177
|
+
Both formats return these plain rows. Group values and `min`/`max` use the
|
|
1178
|
+
field's public representation; counts are numbers, and `sum`/`avg` are numbers
|
|
1179
|
+
except on decimal fields (`precision` and `scale`), which keep the driver's value.
|
|
1180
|
+
NULL sorts as the lowest value on every database.
|
|
1181
|
+
|
|
1182
|
+
#### Security
|
|
1183
|
+
|
|
1184
|
+
`aggregate` checks permissions as a `query` (`method: 'query'`, with
|
|
1185
|
+
`originalContext.method === 'aggregate'`). Row policies, autofilters and other
|
|
1186
|
+
`knexQueryFiltering` hooks run with `queryPurpose: 'aggregate'`. The aggregate reads
|
|
1187
|
+
only the IDs the filtered query selects, so joins added by filters or policies
|
|
1188
|
+
never count a record twice.
|
|
1189
|
+
|
|
1190
|
+
#### Examples
|
|
1191
|
+
|
|
1192
|
+
```javascript
|
|
1193
|
+
const totals = await api.resources.sales.aggregate({
|
|
1194
|
+
groupBy: ['salesperson'],
|
|
1195
|
+
measures: { total: { sum: 'amount' }, count: { count: '*' }, avg: { avg: 'amount' } },
|
|
1196
|
+
queryParams: { filters: { closed: true }, sort: ['-total'], page: { size: 10 } }
|
|
1197
|
+
}, { subject })
|
|
1198
|
+
```
|
|
1199
|
+
|
|
1200
|
+
---
|
|
1201
|
+
|
|
1139
1202
|
## Relationship Methods
|
|
1140
1203
|
|
|
1141
1204
|
### getRelated - Retrieve Related Resources
|
|
@@ -2039,6 +2102,23 @@ database writes; it is not a persisted UI-state field. Use a setter when a
|
|
|
2039
2102
|
virtual input needs to prepare other stored attributes. A `virtualFields` array
|
|
2040
2103
|
is not the resource declaration contract. See [virtual-field behavior](GUIDE/12-field-transformations.md).
|
|
2041
2104
|
|
|
2105
|
+
#### Email and UUID Fields
|
|
2106
|
+
|
|
2107
|
+
`type: 'email'` and `type: 'uuid'` store trimmed strings and reject values that
|
|
2108
|
+
are not an email address or a UUID. Filters with a text operator (`like`,
|
|
2109
|
+
`contains`, `startsWith`, `endsWith`) accept fragments. Registration rejects any
|
|
2110
|
+
type json-rest-schema cannot cast, so a misspelt type fails at `addResource`
|
|
2111
|
+
rather than on the first write.
|
|
2112
|
+
|
|
2113
|
+
#### Retired Fields
|
|
2114
|
+
|
|
2115
|
+
`retired: true` keeps a stored field readable, filterable and sortable while
|
|
2116
|
+
rejecting every value written to it, including `null`, with the `retired_field`
|
|
2117
|
+
rule. `required` and `defaultTo` no longer apply, and PUT leaves its stored value
|
|
2118
|
+
alone. Only stored attributes can be retired, not belongs-to fields. AnyAPI
|
|
2119
|
+
resources can retire fields at run time with
|
|
2120
|
+
[`retireKnexFields`](GUIDE/21-schema-and-migrations.md#retiring-and-migrating-anyapi-fields).
|
|
2121
|
+
|
|
2042
2122
|
#### Field Transformations
|
|
2043
2123
|
|
|
2044
2124
|
Getters and setters receive a value and an injected context. Computed fields
|
|
@@ -286,6 +286,7 @@ Policies are reused for the resource selections that define externally visible d
|
|
|
286
286
|
| --- | --- |
|
|
287
287
|
| `collection` | Top-level collection query, before sorting and pagination. On AnyAPI this filtered builder is also cloned for the matching count. |
|
|
288
288
|
| `count` | A separate count query, including native offset-pagination counts and standalone count helpers. |
|
|
289
|
+
| `aggregate` | The records a resource `aggregate()` call groups and measures. |
|
|
289
290
|
| `single` | Minimal lookup used by `get`, and by existing-record preflights for `put`, `patch`, and `delete`. |
|
|
290
291
|
| `include` | A target resource loaded into JSON:API `included` data. |
|
|
291
292
|
| `search-join` | A related resource used by a cross-resource or polymorphic search, reference filter, or reference sort, including intermediate search path segments. |
|
|
@@ -297,7 +297,8 @@ This optional map may cover a subset of the new stored fields; remaining fields
|
|
|
297
297
|
use the existing allocator. An initial resource map must still cover all stored
|
|
298
298
|
fields. Invalid, occupied or unknown slots and existing field names reject the
|
|
299
299
|
addition. Computed and virtual fields need no slots and cannot have overrides.
|
|
300
|
-
AnyAPI `alterKnexFields` remains explicitly unsupported
|
|
300
|
+
AnyAPI `alterKnexFields` remains explicitly unsupported; change a field's type with
|
|
301
|
+
[`migrateField`](#retiring-and-migrating-anyapi-fields).
|
|
301
302
|
|
|
302
303
|
One call uses one owned metadata transaction. Allocation failure rolls back the
|
|
303
304
|
batch and leaves the runtime schema unchanged. Added getters, setters, defaults,
|
|
@@ -307,26 +308,165 @@ separate DDL behavior.
|
|
|
307
308
|
|
|
308
309
|
Existing rows are not backfilled: unused slots stay null, and defaults apply to
|
|
309
310
|
subsequent writes. Function defaults do not run during allocation. Backfill old
|
|
310
|
-
rows explicitly when needed.
|
|
311
|
-
|
|
311
|
+
rows explicitly when needed.
|
|
312
|
+
|
|
313
|
+
Additions are stored with their search schema, so they survive a restart
|
|
314
|
+
without being declared again. Persisted JSON cannot hold callbacks: getters,
|
|
315
|
+
setters, function defaults, serializers and `compute` functions exist only in the
|
|
316
|
+
process that added them. A server that loads such a field without declaring it
|
|
317
|
+
in code hides the field and logs a warning instead of serving it without its
|
|
318
|
+
callbacks. Declare fields with callbacks in app source on every server; a
|
|
319
|
+
declared field always takes precedence over a stored addition of the same name.
|
|
312
320
|
|
|
313
321
|
Explicit maps now take effect during registration. Repeating `createKnexTable()`
|
|
314
322
|
ensures canonical tables and refreshes schema information without replacing
|
|
315
323
|
field metadata or removing additions.
|
|
316
324
|
|
|
317
|
-
Canonical registration allocates slots from the enriched compiled definitions
|
|
318
|
-
|
|
319
|
-
once per registration
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
order
|
|
325
|
-
the
|
|
325
|
+
Canonical registration allocates slots from the enriched compiled definitions.
|
|
326
|
+
Stored additions that the declaration does not repeat are compiled from their
|
|
327
|
+
persisted JSON. Schema/search hooks run once per registration, `addKnexFields`
|
|
328
|
+
compilation or reload that changes the field set. Repeated table creation does
|
|
329
|
+
not rerun them. Search hook changes therefore survive descriptor refresh.
|
|
330
|
+
|
|
331
|
+
Registration never moves a stored field: existing fields keep their slots
|
|
332
|
+
whatever the declaration order, and new fields take the first free slot. A field
|
|
333
|
+
the declaration omits keeps its mapping while the resource has records, because
|
|
334
|
+
servers still running older code may use it; this declaration stops publishing
|
|
335
|
+
it. Unpopulated omitted fields are released. Changing a populated field's storage
|
|
336
|
+
type is still rejected and needs an explicit data migration. See the
|
|
337
|
+
[schema enrichment migration steps](33-migrating-to-v2.md#schema-enrichment-and-canonical-registration),
|
|
326
338
|
including the consequences for hooks that were previously overwritten.
|
|
327
339
|
|
|
328
|
-
|
|
329
|
-
|
|
340
|
+
Registering an unchanged declaration writes nothing. Concurrent schema changes
|
|
341
|
+
to one resource queue on its metadata row, and a unique index stops two fields
|
|
342
|
+
from sharing a slot. Each committed change increments the resource's
|
|
343
|
+
`schema_version`. On MySQL, the registry runs its own transactions at READ
|
|
344
|
+
COMMITTED, because REPEATABLE READ makes concurrent creation of different
|
|
345
|
+
resources or tenants deadlock. A transaction you pass in keeps its own isolation
|
|
346
|
+
level.
|
|
347
|
+
|
|
348
|
+
### AnyAPI across several servers
|
|
349
|
+
|
|
350
|
+
Each server compiles resources into memory. When one server adds fields, the
|
|
351
|
+
others keep their published schema until they reload it:
|
|
352
|
+
|
|
353
|
+
```js
|
|
354
|
+
await api.use(RestApiAnyapiKnexPlugin, {
|
|
355
|
+
knex,
|
|
356
|
+
tenantId,
|
|
357
|
+
// Runs after each committed change; publish it to the other servers.
|
|
358
|
+
onSchemaChanged: change => notifyServers(change),
|
|
359
|
+
// Safety net for missed notifications.
|
|
360
|
+
schemaCheckInterval: 5000
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
// On a server that receives a notification:
|
|
364
|
+
await api.anyapi.handleSchemaChange(change)
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
`handleSchemaChange({ tenant, resource, version })` reloads the resource only when
|
|
368
|
+
the version is newer than the published one, and returns whether it did.
|
|
369
|
+
`api.anyapi.getSchemaVersion('items')` reads the stored version with one query.
|
|
370
|
+
`api.resources.items.reloadSchema()` reloads unconditionally and resolves to the
|
|
371
|
+
published version. Reloads are read-only: they combine the stored definition
|
|
372
|
+
with this server's declaration and never re-register it.
|
|
373
|
+
|
|
374
|
+
With `schemaCheckInterval`, request methods compare the stored version at most
|
|
375
|
+
once per interval; `0` checks on every request. A request rejected by validation
|
|
376
|
+
also checks the version once and, if the resource changed, reloads and retries.
|
|
377
|
+
A write whose transaction may have committed is never retried. Calls given a
|
|
378
|
+
caller transaction skip both checks, because that transaction may hold
|
|
379
|
+
uncommitted metadata. A rejected `onSchemaChanged` callback is logged; the
|
|
380
|
+
committed change stands.
|
|
381
|
+
|
|
382
|
+
A server can serve resources it never declared. `await api.anyapi.loadResource('items')`
|
|
383
|
+
loads a stored resource, with the resources it relates to, and resolves to it, or to
|
|
384
|
+
`null` when none is stored. `api.anyapi.loadResources()` loads every stored resource
|
|
385
|
+
of the tenant. A stored resource whose non-field options needed callbacks, such as
|
|
386
|
+
`normalizeId`, must be declared in code. To load resources on demand over HTTP,
|
|
387
|
+
call `loadResource` from middleware placed before the API router.
|
|
388
|
+
|
|
389
|
+
### Retiring and migrating AnyAPI fields
|
|
390
|
+
|
|
391
|
+
Slots are never released while records exist, so AnyAPI retires fields instead of
|
|
392
|
+
dropping them:
|
|
393
|
+
|
|
394
|
+
```js
|
|
395
|
+
await api.resources.contacts.retireKnexFields(['phone'])
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
A retired field keeps its slot and its values, stays readable, filterable and
|
|
399
|
+
sortable, and rejects writes as a [`retired: true`](../API.md#retired-fields) field
|
|
400
|
+
does. The mark is stored with the resource definition, so a server that registers
|
|
401
|
+
an older declaration still listing the field keeps it retired. Its name stays taken.
|
|
402
|
+
|
|
403
|
+
`migrateField` changes a field's type by copying its values into a new field:
|
|
404
|
+
|
|
405
|
+
```js
|
|
406
|
+
await api.resources.contacts.migrateField({
|
|
407
|
+
from: 'phone',
|
|
408
|
+
to: { name: 'phoneNumber', type: 'number', nullable: true, search: true },
|
|
409
|
+
transform: (value, { id }) => value === null ? null : Number(value.replace(/\D/g, ''))
|
|
410
|
+
})
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
In one transaction it allocates a slot for the new field, converts every record's
|
|
414
|
+
value, and retires the old field. Each converted value must pass the new field's
|
|
415
|
+
validation; without `transform`, that validation casts the old value directly. A
|
|
416
|
+
value it rejects names the record and rolls everything back. The new field must be
|
|
417
|
+
a plain stored attribute: add fields with callbacks with `addKnexFields` and
|
|
418
|
+
backfill them. Values are written directly, so setters, hooks and record versions
|
|
419
|
+
do not run. Records are converted one by one, which suits per-tenant tables
|
|
420
|
+
better than very large ones. Other servers keep accepting writes to the old field
|
|
421
|
+
until the migration commits, and those writes do not reach the new field, so run
|
|
422
|
+
it while nothing writes the field.
|
|
423
|
+
|
|
424
|
+
Both methods bump `schema_version` and call `onSchemaChanged` once after commit.
|
|
425
|
+
`AnyapiRegistry.retireFields({ tenant, resource, fields }, { transaction })` is the
|
|
426
|
+
deep form of retirement.
|
|
427
|
+
|
|
428
|
+
### AnyAPI tenant instances
|
|
429
|
+
|
|
430
|
+
One API instance per tenant stays cheap when the instances share their start-up work:
|
|
431
|
+
|
|
432
|
+
```js
|
|
433
|
+
// In a migration:
|
|
434
|
+
await ensureAnyApiSchema(knex, { slots: { string: 60, number: 30, boolean: 20, date: 20, json: 10, belongsTo: 15 } })
|
|
435
|
+
|
|
436
|
+
// In the application:
|
|
437
|
+
const registry = new AnyapiRegistry({ knex, cacheSize: 5000 })
|
|
438
|
+
const tenantApi = async tenantId => {
|
|
439
|
+
const api = new JsonRestApi()
|
|
440
|
+
await api.use(RestApiPlugin)
|
|
441
|
+
await api.use(RestApiAnyapiKnexPlugin, { knex, tenantId, registry, ensureSchema: false, ownsKnex: false })
|
|
442
|
+
await api.anyapi.loadResources()
|
|
443
|
+
return api
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
A shared registry reads database capabilities and the slot layout once, and holds
|
|
448
|
+
one descriptor cache, bounded by `cacheSize` (default 100). `ensureSchema: false`
|
|
449
|
+
skips table checks and the logical ID backfill at start-up and in `createKnexTable()`;
|
|
450
|
+
migrations must then run `ensureAnyApiSchema`. `ownsKnex: false` keeps `api.release()`
|
|
451
|
+
from destroying the shared Knex instance. A dropped instance needs no other cleanup.
|
|
452
|
+
|
|
453
|
+
### AnyAPI slot limits
|
|
454
|
+
|
|
455
|
+
Slot limits belong to the database: every server reads the slot columns that
|
|
456
|
+
`any_records` actually has. The defaults are 10 string, 10 number, 5 boolean, 5
|
|
457
|
+
date, 5 JSON and 5 belongsTo slots. `ensureAnyApiSchema(knex, { slots })`, or the
|
|
458
|
+
plugin's `slots` option when it ensures the schema, adds missing columns. Lower
|
|
459
|
+
counts never drop columns. A server whose pool is exhausted rereads the layout
|
|
460
|
+
once, so a migration that adds slots takes effect without a restart.
|
|
461
|
+
|
|
462
|
+
On PostgreSQL, adding a nullable column without a default does not rewrite the
|
|
463
|
+
table. MySQL can add columns instantly from 8.0.29; earlier versions may copy the
|
|
464
|
+
table. MySQL limits a row to 65,535 bytes and each belongsTo slot adds two
|
|
465
|
+
string columns, so check the row size before raising that pool substantially.
|
|
466
|
+
|
|
467
|
+
Deep `AnyapiRegistry.allocateField` and `allocateFields` calls persist the field
|
|
468
|
+
definitions and their allocation together. `allocateFields` adds a batch under one
|
|
469
|
+
version; pass each field's uncompiled `declaration` to store it for other servers. Borrowed transactions own their reads/writes and bypass the
|
|
330
470
|
global descriptor cache; the registry does not commit, roll back or publish their
|
|
331
471
|
uncommitted descriptors globally. After commit, call
|
|
332
472
|
`invalidateDescriptor(tenant, resource)` before an ordinary cached read. Deep
|
|
@@ -62,11 +62,14 @@ changes can be omitted with warnings, and destructive `down` migrations are not
|
|
|
62
62
|
generated automatically. SQLite check/enum changes can require a reviewed table
|
|
63
63
|
rebuild. Native SET fields require an explicit MySQL target.
|
|
64
64
|
|
|
65
|
-
AnyAPI field additions persist slot metadata
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
AnyAPI field additions persist slot metadata and their definitions;
|
|
66
|
+
`alterKnexFields` is unsupported. Registration keeps stored fields in their slots
|
|
67
|
+
and keeps the mapping of populated fields a declaration omits; changing a populated
|
|
68
|
+
field's storage type needs an explicit data migration. Servers sharing a database
|
|
69
|
+
see each other's additions after a reload, notification or schema check, and slot
|
|
70
|
+
limits follow the columns the database has. Old canonical temporal storage needs
|
|
71
|
+
the documented data migration, not just a package update. See
|
|
72
|
+
[table helpers and migration limits](21-schema-and-migrations.md#anyapi-across-several-servers).
|
|
70
73
|
|
|
71
74
|
## Transaction ownership and failures
|
|
72
75
|
|
|
@@ -1038,6 +1038,15 @@ Use `HookHandler<Event, AppContext>` for known hooks and
|
|
|
1038
1038
|
`RuntimeHook<PluginContext>` for arbitrary plugin events; see the
|
|
1039
1039
|
[hook guide](13-hooks-and-lifecycle.md).
|
|
1040
1040
|
|
|
1041
|
+
## Field type names
|
|
1042
|
+
|
|
1043
|
+
`addResource` now rejects a field whose type json-rest-schema cannot cast, naming
|
|
1044
|
+
the field and type. Such fields used to register and then fail on their first
|
|
1045
|
+
write with `No casting function for type`. AnyAPI no longer lists `text`, `float`,
|
|
1046
|
+
`decimal`, `datetime` or `json`, which no storage could write; use `string`,
|
|
1047
|
+
`number`, `dateTime` and `object` or `array`. `email` and `uuid` are now real
|
|
1048
|
+
string types with format checks.
|
|
1049
|
+
|
|
1041
1050
|
## Object and array attributes
|
|
1042
1051
|
|
|
1043
1052
|
Declare structured fields using their public JSON type:
|
|
@@ -1466,6 +1475,16 @@ Missing `fields`, duplicate fields and invalid maps now throw instead of silentl
|
|
|
1466
1475
|
succeeding or leaving unusable metadata. See
|
|
1467
1476
|
[AnyAPI field additions](21-schema-and-migrations.md#anyapi-field-additions).
|
|
1468
1477
|
|
|
1478
|
+
`ensureAnyApiSchema` now adds `schema_version` and `definition_json` to
|
|
1479
|
+
`any_resource_configs` and a unique slot index to `any_field_configs`. It refuses
|
|
1480
|
+
to add the index while two fields of one resource share a slot, naming the slot;
|
|
1481
|
+
reassign one of them first. Deployments using `ensureSchema: false` must run
|
|
1482
|
+
`ensureAnyApiSchema` in a migration. Registration no longer rejects a declaration
|
|
1483
|
+
that omits a populated field or reorders fields: stored slots stay where they
|
|
1484
|
+
are, and omitted populated fields keep their data without being published.
|
|
1485
|
+
Fields added before this release that the declaration does not repeat are kept
|
|
1486
|
+
and published from their stored definitions.
|
|
1487
|
+
|
|
1469
1488
|
Explicit maps take effect during registration, before `createKnexTable()`.
|
|
1470
1489
|
Repeating that method no longer re-registers the initial schema and discards
|
|
1471
1490
|
additions. Keep the expanded declaration and complete map in app source for
|
|
@@ -2805,13 +2824,14 @@ compilation. Canonical metadata no longer invents an attribute declaration for
|
|
|
2805
2824
|
an implicit resource ID; metadata consumers should use `schemaInfo.idProperty`
|
|
2806
2825
|
to identify the resource ID configuration.
|
|
2807
2826
|
|
|
2808
|
-
Enrichment runs once at resource registration,
|
|
2809
|
-
`addKnexFields` compilation
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
schema; use the resource helper
|
|
2813
|
-
|
|
2814
|
-
functions
|
|
2827
|
+
Enrichment runs once at resource registration, once for each canonical
|
|
2828
|
+
`addKnexFields` compilation, and once for a reload that changes the field set.
|
|
2829
|
+
Repeated canonical `createKnexTable()` refreshes storage metadata without
|
|
2830
|
+
rerunning enrichment or restoring removed search fields. Deep registry allocation
|
|
2831
|
+
does not update a running resource's compiled schema; use the resource helper or
|
|
2832
|
+
`reloadSchema()`. Additions survive restarts without being declared again, but
|
|
2833
|
+
persisted JSON cannot restore functions: declare fields with callbacks in source,
|
|
2834
|
+
or servers hide them. Ordinary `addKnexFields` retains its DDL-only behavior.
|
|
2815
2835
|
|
|
2816
2836
|
Fastify now resolves the current cached request contract when validating a
|
|
2817
2837
|
payload. A belongs-to alias added through canonical `addKnexFields` can be used
|
package/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export type * from './types/relationship-methods.js'
|
|
|
6
6
|
export type * from './types/bulk-methods.js'
|
|
7
7
|
export type { ManagedTransaction, ManagedTransactionParam, TransactionMethods } from './types/transactions.js'
|
|
8
8
|
export type * from './types/plugin-options.js'
|
|
9
|
+
export * from './types/anyapi.js'
|
|
9
10
|
export type * from './types/file-detectors.js'
|
|
10
11
|
export type * from './types/fastify-options.js'
|
|
11
12
|
export type * from './types/row-policy.js'
|
package/index.js
CHANGED
|
@@ -13,6 +13,8 @@ export { SocketIOPlugin } from './plugins/core/socketio-plugin.js'
|
|
|
13
13
|
// Database plugins
|
|
14
14
|
export { RestApiKnexPlugin } from './plugins/core/rest-api-knex-plugin.js'
|
|
15
15
|
export { RestApiAnyapiKnexPlugin } from './plugins/core/rest-api-anyapi-knex-plugin.js'
|
|
16
|
+
export { AnyapiRegistry } from './plugins/core/lib/anyapi/anyapi-registry.js'
|
|
17
|
+
export { ensureAnyApiSchema } from './plugins/core/lib/anyapi/schema-utils.js'
|
|
16
18
|
|
|
17
19
|
// Connector plugins
|
|
18
20
|
export { ExpressPlugin } from './plugins/core/connectors/express-plugin.js'
|
package/lib/knex-transaction.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/** @import { OwnedTransaction, TransactionOutcome } from './transaction-types.js' */
|
|
3
3
|
/** @import { StorageDatabase } from '../plugins/core/lib/storage/storage-types.js' */
|
|
4
|
-
/** @typedef {{ cleanupErrors?: Array<{ phase: string, error: unknown }>, [serializableTransaction]?: boolean }} CleanupContext */
|
|
4
|
+
/** @typedef {{ cleanupErrors?: Array<{ phase: string, error: unknown }>, [serializableTransaction]?: boolean, [readCommittedTransaction]?: boolean }} CleanupContext */
|
|
5
5
|
/** @type {WeakMap<OwnedTransaction, (error?: unknown) => Promise<void>>} */
|
|
6
6
|
const transactionConnections = new WeakMap()
|
|
7
7
|
|
|
8
8
|
// Internal connector request; HTTP input cannot select transaction isolation.
|
|
9
9
|
export const serializableTransaction = Symbol('serializableTransaction')
|
|
10
|
+
// MySQL's default REPEATABLE READ gap-locks locking reads of missing rows, so
|
|
11
|
+
// concurrent creators of different rows deadlock on insert. PostgreSQL and
|
|
12
|
+
// SQLite already behave as requested.
|
|
13
|
+
export const readCommittedTransaction = Symbol('readCommittedTransaction')
|
|
10
14
|
|
|
11
15
|
// Own the lease without changing the driver's SQL-completion promise.
|
|
12
16
|
/**
|
|
@@ -30,9 +34,11 @@ export async function createKnexTransaction (knex, context = {}) {
|
|
|
30
34
|
return releasePromise
|
|
31
35
|
}
|
|
32
36
|
try {
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
37
|
+
const client = knex.client.config.client
|
|
38
|
+
const isolationLevel = context[serializableTransaction] === true
|
|
39
|
+
? !['sqlite3', 'better-sqlite3'].includes(client) && 'serializable'
|
|
40
|
+
: context[readCommittedTransaction] === true && ['mysql', 'mysql2'].includes(client) && 'read committed'
|
|
41
|
+
transaction = await knex.transaction({ connection, ...(isolationLevel ? { isolationLevel } : {}) })
|
|
36
42
|
transactionConnections.set(transaction, release)
|
|
37
43
|
transaction.executionPromise.then(
|
|
38
44
|
() => release(),
|