@simtlix/simfinity-js 3.14.0 → 3.15.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/README.md CHANGED
@@ -95,3 +95,24 @@ For tooling that locates the package, use `require.resolve('@simtlix/simfinity-j
95
95
  - [MCP integration](https://simtlix.github.io/simfinity.js/guide/mcp.html)
96
96
  - [Complete library documentation](https://github.com/simtlix/simfinity.js#readme)
97
97
  - [Aggregation examples](./AGGREGATION_EXAMPLE.md)
98
+
99
+ Controller `onSaveCommitted`, `onUpdateCommitted`, `onDeleteCommitted` and `onTransitionCommitted` callbacks run serially after the owning transaction confirms commit and attempts cleanup. `configureAfterCommit({ onError })` configures reporting per runtime; callback failures do not fail the committed write. Borrowed lifecycle writes join an adapter-owned attempt; externally owned native sessions do not provide observed commit notifications. Existing hooks remain transactional. See [the contract, payloads and outbox requirements](https://simtlix.github.io/simfinity.js/guide/controllers.html#after-commit-callbacks).
100
+
101
+ ## Client metadata
102
+
103
+ Since 3.15.0, install the optional `@simtlix/simfinity-descriptor` package,
104
+ aligned with your Simfinity version. It exports `buildClientDescriptor(schema)`
105
+ and `createClientDescriptorHandler(schema, { authorize: 'public' })`. The builder
106
+ describes the exact `createSchema()` result; the explicitly mounted handler can
107
+ instead use an application access function. These functions are not facade/core
108
+ exports. The descriptor does not evaluate dynamic permissions or serialize
109
+ arbitrary validators.
110
+
111
+ Legacy introspection stays enabled by default in 3.x. To opt out, import
112
+ `configureIntrospection` from `@simtlix/simfinity-core/introspection` in a separate
113
+ bootstrap module and call it with `{ fieldExtensions: false }` **before importing
114
+ the runtime or facades**. That direct import requires core as a direct dependency
115
+ pinned to the facade's exact version, including under pnpm or Yarn PnP. A late
116
+ call fails without deleting installed metadata. Migrate clients selecting
117
+ `__Field.extensions` before opting out. See the [descriptor reference](https://simtlix.github.io/simfinity.js/reference/client-descriptor.html)
118
+ for the format, protected HTTP access and composition limits.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simtlix/simfinity-js",
3
- "version": "3.14.0",
3
+ "version": "3.15.0",
4
4
  "description": "MongoDB/Mongoose adapter and compatibility facade for Simfinity GraphQL APIs",
5
5
  "main": "src/index.js",
6
6
  "types": "./types/index.d.ts",
@@ -29,8 +29,8 @@
29
29
  "owner": "simtlix"
30
30
  },
31
31
  "dependencies": {
32
- "@simtlix/simfinity-core": "3.14.0",
33
- "@simtlix/simfinity-mcp": "3.14.0"
32
+ "@simtlix/simfinity-core": "3.15.0",
33
+ "@simtlix/simfinity-mcp": "3.15.0"
34
34
  },
35
35
  "files": [
36
36
  "src",
package/src/index.js CHANGED
@@ -15,6 +15,7 @@ const runtime = createRuntime(mongoAdapter);
15
15
 
16
16
  export const configureQueryLimits = runtime.configureQueryLimits;
17
17
  export const configureMutationLimits = runtime.configureMutationLimits;
18
+ export const configureAfterCommit = runtime.configureAfterCommit;
18
19
  export const connect = runtime.connect;
19
20
  export const addNoEndpointType = runtime.addNoEndpointType;
20
21
  export const createSchema = runtime.createSchema;
@@ -2,6 +2,7 @@ import mongoose from 'mongoose';
2
2
  import { SimfinityError } from '@simtlix/simfinity-core';
3
3
  import { MAX_RECORD_VERSION, isProtectedType, isVersionField, readStoredVersion, retainStoredVersion } from '@simtlix/simfinity-core/internal/record-version';
4
4
  import { getListShape, normalizeConnectionField } from '@simtlix/simfinity-core/internal/relation-storage';
5
+ import { createTransactionCallbacks } from '@simtlix/simfinity-core/internal/transaction-callbacks';
5
6
 
6
7
  import { createMongoModel } from './models.js';
7
8
  import { MONGO_QUERY_OPERATORS, createMongoQueries } from './queries.js';
@@ -255,7 +256,8 @@ const nestedPathOf = (nested) => {
255
256
  };
256
257
 
257
258
  export const createMongoAdapter = (options) => {
258
- const integrity = createMongoIntegrity(options);
259
+ const callbacks = createTransactionCallbacks();
260
+ const integrity = createMongoIntegrity(options, callbacks);
259
261
  const collectionBatching = readCollectionBatching(options);
260
262
  let queries;
261
263
  let getRegistrations = () => [];
@@ -303,8 +305,8 @@ export const createMongoAdapter = (options) => {
303
305
 
304
306
  // A supplied session or model is used as is; only an owned session without a model looks up
305
307
  // the registrations.
306
- const withOwnedTransaction = (session, body, Model, ...rest) => withMongoTransaction(
307
- session, body, (Model || session) ? Model : sessionModel(), ...rest,
308
+ const withOwnedTransaction = (session, body, Model, transactionOptions) => withMongoTransaction(
309
+ session, body, (Model || session) ? Model : sessionModel(), transactionOptions, callbacks,
308
310
  );
309
311
 
310
312
  const versionedRead = (Model, query, many = false) => getRegistrations().some(
@@ -364,6 +366,7 @@ export const createMongoAdapter = (options) => {
364
366
  castId: castObjectId,
365
367
  initialize: integrity.initialize,
366
368
  withTransaction: integrity.enabled ? integrity.withTransaction : withOwnedTransaction,
369
+ enqueueAfterCommit: callbacks.enqueue,
367
370
  newRecord(Model, data, session) {
368
371
  const record = new Model(data);
369
372
  record.$session(session);
@@ -59,7 +59,7 @@ const valuesAt = (value, path) => {
59
59
  // createMongoAdapter's options: the adapter reads the collection batching ones.
60
60
  const ADAPTER_OPTIONS = ['referentialIntegrity', 'batchedCollections', 'collectionBatchSize'];
61
61
 
62
- export const createMongoIntegrity = (options = {}) => {
62
+ export const createMongoIntegrity = (options = {}, callbacks) => {
63
63
  if (!options || typeof options !== 'object' || Array.isArray(options)
64
64
  || Object.keys(options).some((key) => !ADAPTER_OPTIONS.includes(key))) {
65
65
  throw invalid('Expected Mongo adapter options among referentialIntegrity, batchedCollections and collectionBatchSize');
@@ -316,7 +316,7 @@ export const createMongoIntegrity = (options = {}) => {
316
316
  withTransaction(session, body, Model) {
317
317
  assertReady();
318
318
  if (session) assertSession(session);
319
- return withMongoTransaction(session, body, Model || models.values().next().value, transactionOptions);
319
+ return withMongoTransaction(session, body, Model || models.values().next().value, transactionOptions, callbacks);
320
320
  },
321
321
  async saveRecord(Model, record, session) {
322
322
  assertWrite(Model, session);
@@ -46,7 +46,7 @@ const endOwnedSession = async (session, failed) => {
46
46
  }
47
47
  };
48
48
 
49
- export const withMongoTransaction = async (session, body, model, transactionOptions) => {
49
+ export const withMongoTransaction = async (session, body, model, transactionOptions, callbacks) => {
50
50
  const connection = model?.db || mongoose.connection;
51
51
  if (session) {
52
52
  if (!session.inTransaction()) {
@@ -61,15 +61,21 @@ export const withMongoTransaction = async (session, body, model, transactionOpti
61
61
  const mySession = connection === mongoose.connection
62
62
  ? await mongoose.startSession() : await connection.startSession();
63
63
  let failed = false;
64
+ let confirmed;
64
65
  try {
65
66
  for (let attempt = 0; ; attempt++) {
66
67
  if (transactionOptions) await mySession.startTransaction(transactionOptions);
67
68
  else await mySession.startTransaction();
69
+ const effects = callbacks?.begin(mySession);
68
70
  try {
69
71
  const result = await body(mySession);
72
+ effects?.seal();
70
73
  await commitWithRetry(mySession);
74
+ effects?.confirm();
75
+ confirmed = effects;
71
76
  return result;
72
77
  } catch (error) {
78
+ effects?.discard();
73
79
  if (error?.errorLabels?.includes('UnknownTransactionCommitResult')) {
74
80
  throw error;
75
81
  }
@@ -96,6 +102,7 @@ export const withMongoTransaction = async (session, body, model, transactionOpti
96
102
  failed = true;
97
103
  throw error;
98
104
  } finally {
99
- await endOwnedSession(mySession, failed);
105
+ // Cleanup can reject after commit. The confirmed effects still run, outside every retry catch.
106
+ try { await endOwnedSession(mySession, failed); } finally { await confirmed?.drain(); }
100
107
  }
101
108
  };
package/types/index.d.ts CHANGED
@@ -13,6 +13,8 @@ import type {
13
13
  GraphQLOutputType,
14
14
  } from 'graphql';
15
15
  import type {
16
+ AfterCommitOptions,
17
+ CommittedControllerCallbacks,
16
18
  CreateSchemaOptions,
17
19
  DatabaseAdapter,
18
20
  MutationLimitsOptions,
@@ -36,6 +38,9 @@ export {
36
38
  validators,
37
39
  } from '@simtlix/simfinity-core';
38
40
  export type {
41
+ AfterCommitErrorDetails,
42
+ AfterCommitOptions,
43
+ CommittedControllerCallbacks,
39
44
  AuthPluginOptions,
40
45
  AuthRule,
41
46
  AuthRuleFunction,
@@ -155,8 +160,8 @@ export function getRegistrations(): RuntimeRegistration[];
155
160
  * Core schema-building API (src/index.js)
156
161
  * ========================================================================== */
157
162
 
158
- /** Lifecycle hooks run before commit and may repeat on a transient transaction retry. */
159
- export interface EntityController {
163
+ /** Lifecycle hooks run before commit; the inherited committed callbacks run after confirmed commit. */
164
+ export interface EntityController extends CommittedControllerCallbacks {
160
165
  onSaving?(doc: any, args: any, session: any, context: any): void | Promise<void>;
161
166
  onSaved?(result: any, args: any, session: any, context: any): void | Promise<void>;
162
167
  onUpdating?(id: any, args: any, session: any, context: any): void | Promise<void>;
@@ -298,6 +303,9 @@ export function preventCreatingCollection(prevent: boolean): void;
298
303
  */
299
304
  export function configureQueryLimits(options?: QueryLimitsOptions): void;
300
305
 
306
+ /** Configure the singleton runtime's best-effort reporting of after-commit callback failures. */
307
+ export function configureAfterCommit(options?: AfterCommitOptions): void;
308
+
301
309
  /**
302
310
  * Process-wide limit on nested added/updated/deleted entries per generated add, update or
303
311
  * state-action mutation; unlimited by default, restored by calling it without options.