@simtlix/simfinity-core 3.14.0 → 3.16.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/types/index.d.ts CHANGED
@@ -56,11 +56,18 @@ export const MAX_RECORD_VERSION: number;
56
56
  * missing; constructs that middleware and scopes add read stored data. With any other adapter they
57
57
  * fail with `UNSUPPORTED_QUERY`. A term or group condition without a path tests the field itself, except that every term
58
58
  * of a quantified relation filter needs a non-empty element-relative path.
59
+ *
60
+ * `IIN` is `IN` ignoring case: an array of non-null strings without U+0000, possibly empty, on a `String` field or
61
+ * `String` list by its effective scalar; it matches where `IEQ` matches one of them, and `[]` matches nothing.
62
+ * `CONTAINS_ALL` takes a non-empty array of non-null scalars, cast like `IN` elements; it applies to a scalar list or a
63
+ * field reached through embedded lists, where it holds when every value is among the values of all items, and fails
64
+ * with `INVALID_FILTER_OPERATOR` on other leaves and with `INVALID_FILTER_PATH` through a collection or a reference
65
+ * inside an embedded list. Neither has a negated form.
59
66
  */
60
67
  export type QueryOperator =
61
68
  | 'EQ' | 'NE' | 'LT' | 'LTE' | 'GT' | 'GTE' | 'BTW' | 'IN' | 'NIN' | 'LIKE'
62
69
  | 'ILIKE' | 'STARTS_WITH' | 'ISTARTS_WITH' | 'ENDS_WITH' | 'IENDS_WITH' | 'IEQ'
63
- | 'IS_EMPTY' | 'IS_NOT_EMPTY';
70
+ | 'IS_EMPTY' | 'IS_NOT_EMPTY' | 'CONTAINS_ALL' | 'IIN';
64
71
 
65
72
  /** The quantifier applied to the elements visible in a collection or reference frame. */
66
73
  export type QueryQuantifier = 'ANY' | 'ALL' | 'NONE';
@@ -333,7 +340,27 @@ export class InternalServerError extends SimfinityError {
333
340
  */
334
341
  export function buildErrorFormatter(callback?: (error: SimfinityError) => Error | void): (error: unknown) => GraphQLError;
335
342
 
336
- export interface EntityController<Session = any> {
343
+ export interface AfterCommitErrorDetails {
344
+ committed: true;
345
+ typeName: string;
346
+ operation: 'save' | 'update' | 'delete' | 'state_changed';
347
+ callbackName: 'onSaveCommitted' | 'onUpdateCommitted' | 'onDeleteCommitted' | 'onTransitionCommitted';
348
+ context: any;
349
+ }
350
+ export interface AfterCommitOptions {
351
+ /** Operational reporting only. Rejections are caught and never fail the committed write. */
352
+ onError?: (error: unknown, details: AfterCommitErrorDetails) => void | Promise<void>;
353
+ }
354
+ export interface CommittedControllerCallbacks {
355
+ /** After the owning transaction commits; a detached result and the successful attempt's input. */
356
+ onSaveCommitted?(record: any, args: any, context: any): void | Promise<void>;
357
+ onUpdateCommitted?(record: any, context: any): void | Promise<void>;
358
+ /** Called only when a record was actually deleted. */
359
+ onDeleteCommitted?(record: any, context: any): void | Promise<void>;
360
+ /** Exclusive notification for a state action; action is its registered name. */
361
+ onTransitionCommitted?(record: any, action: string, context: any): void | Promise<void>;
362
+ }
363
+ export interface EntityController<Session = any> extends CommittedControllerCallbacks {
337
364
  onSaving?(record: any, args: any, session: Session | undefined, context: any): void | Promise<void>;
338
365
  onSaved?(record: any, args: any, session: Session | undefined, context: any): void | Promise<void>;
339
366
  onUpdating?(id: any, update: any, session: Session | undefined, context: any): void | Promise<void>;
@@ -407,16 +434,24 @@ export interface ConcurrencyDeclaration {
407
434
  }
408
435
 
409
436
  /**
410
- * Process-wide limits for generated add, update and state-action mutations. Any other option,
411
- * such as a misspelled `maxNestedOperation`, throws `INVALID_MUTATION_LIMITS` (400).
437
+ * Process-wide limits for generated mutations. Each call replaces both values: an omitted or
438
+ * `undefined` option restores its default. Any other option, such as a misspelled
439
+ * `maxNestedOperation`, throws `INVALID_MUTATION_LIMITS` (400) and keeps the current limits.
412
440
  */
413
441
  export interface MutationLimitsOptions {
414
442
  /**
415
443
  * Maximum number of `added`, `updated` and `deleted` entries across every nested level of
416
444
  * non-embedded collection fields in one generated mutation. A non-negative safe integer;
417
- * `null` or omitted means unlimited (the default).
445
+ * `null` or omitted means unlimited (the default). The helpers `updateObject` and
446
+ * `transitionObject` apply it to their `input` as the generated mutations do.
418
447
  */
419
448
  maxNestedOperations?: number | null;
449
+ /**
450
+ * Maximum number of operations in one generated transaction mutation. A positive safe integer;
451
+ * omitted means 100 (the default). `null`, `0`, a negative number or a non-integer throws
452
+ * `INVALID_MUTATION_LIMITS` (400).
453
+ */
454
+ maxTransactionOperations?: number;
420
455
  }
421
456
 
422
457
  /**
@@ -429,6 +464,21 @@ export interface CreateSchemaOptions {
429
464
  * `[]` includes none, and names that are not registered are ignored.
430
465
  */
431
466
  includedCustomQueries?: string[] | null;
467
+ /**
468
+ * Adds one mutation field that runs a list of generated mutations in order in one database
469
+ * transaction. `null` or omission adds none. `name` defaults to `transaction` and must be a GraphQL
470
+ * name that does not start with `__`; it names the field and its types (for `transaction`:
471
+ * `TransactionOperation`, a `@oneOf` input keyed by operation, `TransactionResult` and, for deletes
472
+ * of types without optimistic concurrency, `TransactionDeleteInput`). The operations are the
473
+ * generated add, update, delete and state machine action mutations of `types` that this schema
474
+ * includes; registered mutations never are. `types` is a non-empty array of distinct types registered
475
+ * in this runtime. An invalid option throws `INVALID_SCHEMA_OPTIONS` (500) before models are prepared,
476
+ * or once the generated mutations are built when the field name or a derived type name is taken or
477
+ * no operation remains; a type bound to another runtime throws `TYPE_BOUND_TO_OTHER_RUNTIME` (409),
478
+ * and an adapter without `assertTransactionModels` throws `TRANSACTION_UNSUPPORTED` (500). A failed
479
+ * `createSchema()` keeps no transaction field or type.
480
+ */
481
+ transaction?: { name?: string; types: GraphQLObjectType[] } | null;
432
482
  }
433
483
 
434
484
  export interface QueryLimitsOptions {
@@ -485,13 +535,17 @@ export interface ViewOptions {
485
535
  }
486
536
 
487
537
  export interface Runtime<Model = any, Session = any> {
538
+ /** Per runtime. No arguments resets to safe default logging; invalid options leave it unchanged. */
539
+ configureAfterCommit(options?: AfterCommitOptions): void;
488
540
  /** The same process-wide setting as the root `configureQueryLimits`. */
489
541
  configureQueryLimits(options?: QueryLimitsOptions): void;
490
542
  /**
491
- * Process-wide; calling it without options restores the unlimited default. Invalid options,
492
- * including unknown or misspelled option keys, throw `INVALID_MUTATION_LIMITS` (400) and keep
493
- * the current limit. A generated mutation over the limit fails with
494
- * `NESTED_OPERATIONS_EXCEEDED` (400) before its transaction starts.
543
+ * Process-wide; calling it without options restores both defaults (`maxNestedOperations`
544
+ * unlimited, `maxTransactionOperations` 100). Invalid options, including unknown or misspelled
545
+ * option keys, throw `INVALID_MUTATION_LIMITS` (400) and keep the current limits. A generated
546
+ * mutation over the nested limit fails with `NESTED_OPERATIONS_EXCEEDED` (400) before its
547
+ * transaction starts. `updateObject` and `transitionObject` apply `maxNestedOperations` to their
548
+ * `input` in the same way (`null` = no limit).
495
549
  */
496
550
  configureMutationLimits(options?: MutationLimitsOptions): void;
497
551
  /**
@@ -539,7 +593,8 @@ export interface Runtime<Model = any, Session = any> {
539
593
  *
540
594
  * `options.includedCustomQueries` selects custom queries by name (see {@link CreateSchemaOptions});
541
595
  * an included custom query named like an included generated query throws `QUERY_NAME_CONFLICT`
542
- * (500). A schema without mutation fields has no `Mutation` root.
596
+ * (500). `options.transaction` adds the generated transaction field (see {@link CreateSchemaOptions}).
597
+ * A schema without mutation fields has no `Mutation` root.
543
598
  */
544
599
  createSchema(includedQueryTypes?: GraphQLObjectType[] | null, includedMutationTypes?: GraphQLObjectType[] | null, includedCustomMutations?: string[] | null, options?: CreateSchemaOptions): GraphQLSchema;
545
600
  getModel(type: GraphQLObjectType | { name: string }): Model | null | undefined;
@@ -580,6 +635,64 @@ export interface Runtime<Model = any, Session = any> {
580
635
  registerView(name: string, options: ViewOptions): void;
581
636
  /** Owns the full workflow transaction unless an active caller session is supplied. */
582
637
  saveObject(typeName: string, args: Record<string, any>, session?: Session, context?: any): Promise<any>;
638
+ /**
639
+ * Runs the lifecycle of the generated update mutation from code: validators, controller hooks,
640
+ * optimistic concurrency and the nested `added`, `updated` and `deleted` entries, in one transaction.
641
+ * `typeName` names a type registered in this runtime that stores records and has its update input
642
+ * prepared by `createSchema()`, with an `id` field. Whether a schema exposes the mutation does not
643
+ * matter: the helper is not an authorization boundary. Any other `typeName` rejects with
644
+ * `INVALID_OPERATION_TARGET` (500), before the input is read.
645
+ *
646
+ * `input` has the shape of the type's generated update input, `version` included on a type with
647
+ * optimistic concurrency. It is checked structurally and copied before anything runs, and the
648
+ * caller's object is never written to. It is not coerced like a GraphQL input: enum fields take
649
+ * their internal values, scalars are not parsed, a reference is its record key as a string, such as
650
+ * `{ id: String(record._id) }`, and the `state` of a type with a state machine changes only through
651
+ * `transitionObject`. A scalar field takes a primitive, a `Date` or plain data (objects and arrays,
652
+ * copied deeply, at most 256 levels deep); class instances such as `ObjectId`, `Buffer` or `Map` are
653
+ * rejected with `INVALID_INPUT`. An own `undefined` counts as omitted, and a bare value is never
654
+ * wrapped into a list. A missing `input`, `id` or value for a non-null field rejects with
655
+ * `REQUIRED_VALUE` (400); any other violation, such as an unknown field, an object that is not plain
656
+ * or a list that is not dense, rejects with `INVALID_INPUT` (400). The message names the path, such
657
+ * as `input.lines.added[1].price`, and never a value. More nested entries than `maxNestedOperations`
658
+ * from `configureMutationLimits()` (`null` = no limit) reject with `NESTED_OPERATIONS_EXCEEDED` (400)
659
+ * before the transaction starts.
660
+ *
661
+ * Without `session`, the adapter owns the transaction and its retries. A supplied `session` must have
662
+ * an active transaction, and the helper itself never commits, retries, aborts or cleans up a supplied
663
+ * session; an adapter may still abort it on a write error (for example MongoDB with transactional
664
+ * reference integrity). Like `saveObject`, it skips the global middleware of the record itself; nested entries keep theirs and
665
+ * receive `context`, which may be `undefined`.
666
+ *
667
+ * Resolves to the updated record, as the generated mutation returns it. An `id` that has no record
668
+ * rejects with `NOT_VALID_ID` (404), and a stale `version` with `VERSION_CONFLICT` (409).
669
+ */
670
+ updateObject(typeName: string, input: Record<string, any>, session?: Session, context?: any): Promise<any>;
671
+ /**
672
+ * Runs the lifecycle of the generated delete mutation from code. `target` is `{ id }`, the record key
673
+ * as a string, plus `version`, the version read, on a type with optimistic concurrency; on any other
674
+ * type `version` is an unknown field. The type is checked as in {@link Runtime.updateObject}, except
675
+ * that it needs no update input of its own; the type must still be a stored record type, which only
676
+ * `createSchema()` establishes. The target is checked before anything runs: a missing
677
+ * target, `id` or required `version` rejects with `REQUIRED_VALUE` (400), and any other violation
678
+ * with `INVALID_INPUT` (400). A stale `version` rejects with `VERSION_CONFLICT` (409). Sessions,
679
+ * `context` and middleware work as in `updateObject`.
680
+ *
681
+ * Resolves to the deleted record, or to `null` when no record has that ID.
682
+ */
683
+ deleteObject(typeName: string, target: { id: string; version?: number }, session?: Session, context?: any): Promise<any>;
684
+ /**
685
+ * Runs the state machine action `actionName` of `typeName` from code, with the lifecycle of the
686
+ * generated action mutation: the check of the current state, the action's callback, the update and the
687
+ * controller hooks, in one transaction. An `actionName` that is not an own action of the type's state
688
+ * machine, or a type without one, rejects with `UNKNOWN_ACTION` (500). The type, the checks of `input`,
689
+ * the session, `context`, the middleware and the errors are as in {@link Runtime.updateObject}. `input` has
690
+ * the shape of the update input and does not carry `state`. An action that does not start from the
691
+ * record's state rejects with `BAD_REQUEST` (400).
692
+ *
693
+ * Resolves to the updated record, with `state` set to the action's target state.
694
+ */
695
+ transitionObject(typeName: string, actionName: string, input: Record<string, any>, session?: Session, context?: any): Promise<any>;
583
696
  preventCreatingCollection(prevent: boolean): void;
584
697
  }
585
698
 
@@ -594,10 +707,17 @@ export interface DatabaseAdapter<Model = any, Session = any> {
594
707
  * by-ID read through a `get_by_id` scope, whose arguments use a `QLOperator` value the adapter
595
708
  * does not support fails with `UNSUPPORTED_QUERY` (400), before middleware runs for the client's
596
709
  * arguments and before `find`, `count`, `aggregate`, `findChildren` or `findChildrenBatch` for
597
- * conditions that middleware, scopes and joined scopes added. Operators outside `QLOperator` still
598
- * reach the adapter, which reports them as before. Declaring `IS_EMPTY` and `IS_NOT_EMPTY` means
599
- * compiling their semantics on the record's own fields; emptiness through references and
600
- * collections also needs `referenceEmptiness: true`, and fails with `UNSUPPORTED_QUERY` without it.
710
+ * conditions that middleware, scopes and joined scopes added. Operators outside `QLOperator`, which
711
+ * only trusted code can write, fail with `INVALID_FILTER_OPERATOR` (400) before the adapter is
712
+ * called, except in the forms the compilers ignore: an operator beside the `terms` of an object
713
+ * filter, and the `terms` of a scalar, scalar-list, interface or union field's filter. Those reach
714
+ * the adapter, which ignores them. Declaring `IS_EMPTY` and `IS_NOT_EMPTY` means compiling their
715
+ * semantics on the record's own fields; emptiness through references and collections also needs
716
+ * `referenceEmptiness: true`, and fails with `UNSUPPORTED_QUERY` without it.
717
+ * Declaring `CONTAINS_ALL` means compiling set containment over the leaf's values, including the
718
+ * values of every item of embedded lists, two-valued under negation; declaring `IIN` means
719
+ * compiling case-insensitive membership under the case-folding contract of the text operators.
720
+ * Names that an older version ignored are honored once this version knows them.
601
721
  */
602
722
  queryFeatures?: QueryFeatures | null | undefined;
603
723
  /**
@@ -609,6 +729,16 @@ export interface DatabaseAdapter<Model = any, Session = any> {
609
729
  * contract of `update` and `delete` and to the requirements on `find` below.
610
730
  */
611
731
  writeFeatures?: { optimisticConcurrency?: boolean } | null | undefined;
732
+ /**
733
+ * Optional, and required to enable the generated transaction field (see
734
+ * `CreateSchemaOptions.transaction`). Receives the models one transaction of that field may
735
+ * write: those of the types that own its operations and of the referenced collection items their
736
+ * inputs declare, at any depth, without duplicates. Core calls it before each transaction of the
737
+ * field opens, never in `createSchema()`, so a schema can be created before the connection opens.
738
+ * It must be synchronous and never write, and it throws when the models cannot share one session; a
739
+ * returned promise fails the transaction with `TRANSACTION_UNSUPPORTED` (500).
740
+ */
741
+ assertTransactionModels?(models: Model[]): void;
612
742
  bind?(runtime: Pick<Runtime<Model, Session>, 'getModel' | 'getType' | 'getRegistrations'>): void;
613
743
  validateRegistration?(registration: RuntimeRegistration<Model, Session>): void;
614
744
  /**
@@ -638,6 +768,15 @@ export interface DatabaseAdapter<Model = any, Session = any> {
638
768
  castId(value: any): any;
639
769
  stateValue?(state: { name: string; value: any }): any;
640
770
  withTransaction<T>(session: Session | null | undefined, callback: (session: Session) => Promise<T> | T, model?: Model): Promise<T>;
771
+ /**
772
+ * Optional capability for controller callbacks. Accept only into an active attempt owned by this
773
+ * adapter; false means the supplied session has no owned attempt. A new queue is required per
774
+ * retry, including retries on the same native session. Seal before commit, discard on abort or
775
+ * unresolved uncertainty, and drain once, serially, after confirmed commit and cleanup attempt.
776
+ * Callback failures must never enter transaction retry/rollback paths or fail a committed write.
777
+ * Borrowed operations join the owning attempt; this method does not observe external commits.
778
+ */
779
+ enqueueAfterCommit?(session: Session, run: () => void | Promise<void>): boolean;
641
780
  newRecord(model: Model, data: any, session?: Session): any;
642
781
  saveRecord(model: Model, record: any, session?: Session): any;
643
782
  toObject(record: any): any;
@@ -760,6 +899,8 @@ export type QueryJoins = Record<string, QueryPredicate | null>;
760
899
 
761
900
  /**
762
901
  * `operator` is a {@link QueryOperator}; the value of a text operator is a validated string. For
902
+ * `CONTAINS_ALL` the value is a non-empty array of non-null scalars, validated and not cast; the
903
+ * compiler casts each element as for `IN`. For `IIN` it is an array of strings, possibly empty. For
763
904
  * `IS_EMPTY` and `IS_NOT_EMPTY` the value is null, and `path` may end at an embedded object, an
764
905
  * embedded list or a scalar list as well as at a scalar; `IS_NOT_EMPTY` is the exact complement,
765
906
  * which the compiler builds. With `referenceEmptiness: true`, their `path` may also cross single
@@ -818,6 +959,22 @@ export interface QueryPlan {
818
959
  * object fails with `INVALID_FILTER_PATH`, as on MongoDB; a bare object field keeps
819
960
  * `MISSING_FILTER_PATH`.
820
961
  *
962
+ * `CONTAINS_ALL` and `IIN` are checked in this order: the path (`AMBIGUOUS_FILTER_FIELD`,
963
+ * `INVALID_FILTER_PATH`, `INVALID_FILTER_FIELD`) and the object-end rule above, then an unknown name
964
+ * (`INVALID_FILTER_OPERATOR`) and `operators` (`UNSUPPORTED_QUERY`), then, for `CONTAINS_ALL` only,
965
+ * `INVALID_FILTER_PATH` through a collection, aggregate join prefixes included
966
+ * (`The shorthand excludes them; use an explicit group`), `INVALID_FILTER_PATH` through a reference
967
+ * inside an embedded list, and `INVALID_FILTER_OPERATOR` on a leaf that is neither a scalar list nor
968
+ * a scalar reached through an embedded list, and the values last, with `INVALID_FILTER_VALUE`.
969
+ * `CONTAINS_ALL` takes a non-empty array of non-null scalars, none of them an array or a plain
970
+ * object; `IIN` takes an array of strings without U+0000, possibly empty, on a `String` field or list
971
+ * by its effective scalar. A hole of a sparse array fails for both, while `IN`, `NIN` and `BTW` keep
972
+ * skipping holes.
973
+ * Both plan `{ kind: 'predicate', path, operator, value }` with the array as validated, neither cast
974
+ * nor deduplicated. `IIN` through a collection plans as `IN` does: an implicit `ANY` frame with
975
+ * `quantifiers`, a row filter on an aggregate's join prefix, a legacy join otherwise. `CONTAINS_ALL`
976
+ * is never cut into a frame.
977
+ *
821
978
  * `referenceEmptiness` defaults to true when `operators` is omitted and to false when it is given.
822
979
  * When it is false, an emptiness condition that crosses or ends at a reference or a collection, or
823
980
  * reads a reference or collection member, fails with `UNSUPPORTED_QUERY` where the order above has
@@ -826,8 +983,17 @@ export interface QueryPlan {
826
983
  * becomes the predicate's `scopes`. `quantifiers` has the same default rule and requires reference
827
984
  * emptiness; enabled collection filters emit quantified nodes, with per-group/prefix correlation,
828
985
  * element-relative scopes/joins and aggregate anchors beyond legacy J joins. `__scope`, `__implicit`
829
- * and `__anchor` are reserved internal input metadata. A marker not filled for every path fails
830
- * with `DETACHED_CLIENT_FILTER` (500).
986
+ * and `__anchor` are reserved internal input metadata. A group that trusted code writes with
987
+ * `__implicit: true` must, after its `__anchor`, end at a collection, or at a reference without
988
+ * crossing a list, and its `__anchor`, when present, must have exactly the shape the collection
989
+ * shorthand writes: in an aggregate, on a group at the root, a non-empty array of field names without
990
+ * dots that crosses at least one collection, each a join prefix, followed by a `path` of one
991
+ * collection. Otherwise the group fails with `INVALID_FILTER_PATH` (400), whatever operators it holds
992
+ * and before its conditions are checked, with the message
993
+ * `An implicit quantified path must end at a collection, or at a reference without crossing a list` or
994
+ * `An implicit quantified anchor must name the aggregate's join prefixes`; the anchor's form is
995
+ * checked before the path rule, the rest after it.
996
+ * A marker not filled for every path fails with `DETACHED_CLIENT_FILTER` (500).
831
997
  */
832
998
  export function createQueryPlan(models: ModelDescription, entityName: string, input?: Record<string, any>, options?: { mode?: QueryPlan['mode']; operators?: Iterable<string>; referenceEmptiness?: boolean; quantifiers?: boolean }): QueryPlan;
833
999
  export function resolveModelPath(models: ModelDescription, entityName: string, path: string | string[]): FieldDescription[];
@@ -843,7 +1009,7 @@ export function resolveModelPath(models: ModelDescription, entityName: string, p
843
1009
  export function configureQueryLimits(options?: QueryLimitsOptions): void;
844
1010
  /**
845
1011
  * Same process-wide setting as `Runtime.configureMutationLimits`. Unknown or misspelled options
846
- * throw `INVALID_MUTATION_LIMITS` (400) and keep the current limit.
1012
+ * throw `INVALID_MUTATION_LIMITS` (400) and keep the current limits.
847
1013
  */
848
1014
  export function configureMutationLimits(options?: MutationLimitsOptions): void;
849
1015
  export function paginationStages(pagination: { page: number; size: number } | null | undefined, withDefault: boolean): Array<{ $skip: number } | { $limit: number }>;
@@ -1093,3 +1259,6 @@ export const plugins: {
1093
1259
  };
1094
1260
  };
1095
1261
  };
1262
+
1263
+ /** Early opt-out. Import its function from /introspection before loading the runtime. */
1264
+ export interface IntrospectionOptions { fieldExtensions: false; }
@@ -0,0 +1,4 @@
1
+ import type { IntrospectionOptions } from './index.js';
2
+ export type { IntrospectionOptions } from './index.js';
3
+ /** Must run before importing the runtime/facades. Never removes installed metadata. */
4
+ export function configureIntrospection(options: IntrospectionOptions): void;