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.
Files changed (29) hide show
  1. package/docs/API.md +80 -0
  2. package/docs/GUIDE/17-row-policies.md +1 -0
  3. package/docs/GUIDE/21-schema-and-migrations.md +154 -14
  4. package/docs/GUIDE/30-backend-capabilities.md +8 -5
  5. package/docs/GUIDE/33-migrating-to-v2.md +27 -7
  6. package/index.d.ts +1 -0
  7. package/index.js +2 -0
  8. package/lib/knex-transaction.js +10 -4
  9. package/package.json +1 -1
  10. package/plugins/core/lib/anyapi/anyapi-registry.js +569 -408
  11. package/plugins/core/lib/anyapi/schema-utils.js +101 -51
  12. package/plugins/core/lib/anyapi/stored-definition.js +112 -0
  13. package/plugins/core/lib/querying/aggregate-query.js +170 -0
  14. package/plugins/core/lib/querying-writing/compile-schemas.js +17 -0
  15. package/plugins/core/lib/querying-writing/database-value-normalizers.js +16 -0
  16. package/plugins/core/lib/querying-writing/default-data-helpers.js +4 -0
  17. package/plugins/core/lib/querying-writing/field-types.js +43 -0
  18. package/plugins/core/lib/querying-writing/request-contracts.js +1 -1
  19. package/plugins/core/lib/storage/canonical-data-helpers.js +29 -0
  20. package/plugins/core/lib/storage/ordinary-data-helpers.js +25 -0
  21. package/plugins/core/lib/storage/storage-types.d.ts +21 -0
  22. package/plugins/core/rest-api-anyapi-knex-plugin.js +331 -54
  23. package/plugins/core/rest-api-plugin-methods/aggregate.js +41 -0
  24. package/plugins/core/rest-api-plugin-methods/common.js +16 -1
  25. package/plugins/core/rest-api-plugin.js +2 -0
  26. package/types/anyapi.d.ts +32 -0
  27. package/types/plugin-options.d.ts +12 -0
  28. package/types/resource-methods.d.ts +19 -0
  29. 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. Declare the expanded schema and complete slot map
311
- in app source for restart, including callbacks that persisted JSON cannot store.
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
- it does not compile a second schema from persisted JSON. Schema/search hooks run
319
- once per registration or `addKnexFields` compilation. Repeated table creation
320
- does not rerun them. Search hook changes therefore survive descriptor refresh.
321
-
322
- Re-registration rejects removing or remapping existing stored fields while the
323
- resource has records. Preserve a complete `canonicalFieldsMap` when declaration
324
- order changes, and perform an explicit data migration for layout changes. See
325
- the [schema enrichment migration steps](33-migrating-to-v2.md#schema-enrichment-and-canonical-registration),
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
- Deep `AnyapiRegistry.allocateField` calls persist the field definition and its
329
- allocation together. Borrowed transactions own their reads/writes and bypass the
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; `alterKnexFields` is unsupported.
66
- Re-registering populated resources cannot remove or remap stored fields. Preserve
67
- slot maps and migrate stored data explicitly when changing layouts. Old canonical
68
- temporal storage needs the documented data migration, not just a package update.
69
- See [table helpers and migration limits](21-schema-and-migrations.md).
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, and once for each canonical
2809
- `addKnexFields` compilation. Repeated canonical `createKnexTable()` refreshes
2810
- storage metadata without rerunning enrichment or restoring removed search
2811
- fields. Deep registry allocation does not update a running resource's compiled
2812
- schema; use the resource helper and retain its expanded declaration in source.
2813
- Declare callbacks in source again after restart: persisted JSON cannot restore
2814
- functions. Ordinary `addKnexFields` retains its DDL-only behavior.
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'
@@ -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 serializable = context[serializableTransaction] === true
34
- const sqlite = ['sqlite3', 'better-sqlite3'].includes(knex.client.config.client)
35
- transaction = await knex.transaction({ connection, ...(serializable && !sqlite ? { isolationLevel: 'serializable' } : {}) })
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(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "json-rest-api",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "main": "index.js",
5
5
  "types": "index.d.ts",
6
6
  "type": "module",