@simtlix/simfinity-core 3.9.0 → 3.11.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
@@ -21,10 +21,26 @@ export const QLValue: GraphQLScalarType<unknown, unknown>;
21
21
  * and `IENDS_WITH` as a suffix, respecting or ignoring case, and `IEQ` as the whole value ignoring
22
22
  * case. An empty value matches every non-null string, except for `IEQ`, where it matches only the
23
23
  * empty string. On a list, some element must match.
24
+ *
25
+ * `IS_EMPTY` and `IS_NOT_EMPTY` take no value (omitted or null; any other value fails with
26
+ * `INVALID_FILTER_VALUE`), and every record matches exactly one of them. `IS_EMPTY` holds for a
27
+ * missing, null or `""` String (`" "` is not empty), a missing or null value of any other scalar or
28
+ * enum, a missing, null or `[]` list (`[null]` and `[""]` are not empty), and a missing or null
29
+ * embedded object or one whose stored members are all empty by these rules; the record identity is
30
+ * never empty. The target's `id` right after a reference is empty exactly when the reference is,
31
+ * including when the target is missing or hidden by its `find` scope. A path that continues inside an
32
+ * embedded list or a collection fails with `INVALID_FILTER_PATH`. With an adapter that declares
33
+ * `queryFeatures.referenceEmptiness: true`, a path may also cross single references and end at a
34
+ * reference or a collection, and an embedded object may have reference and collection members: a
35
+ * reference is empty when no record it points to exists that the target's `find` scope admits for
36
+ * the caller, a collection when no such child exists, and a hidden record on the way reads as
37
+ * missing; constructs that middleware and scopes add read stored data. With any other adapter they
38
+ * fail with `UNSUPPORTED_QUERY`. A term or group condition without a path tests the field itself.
24
39
  */
25
40
  export type QueryOperator =
26
41
  | 'EQ' | 'NE' | 'LT' | 'LTE' | 'GT' | 'GTE' | 'BTW' | 'IN' | 'NIN' | 'LIKE'
27
- | 'ILIKE' | 'STARTS_WITH' | 'ISTARTS_WITH' | 'ENDS_WITH' | 'IENDS_WITH' | 'IEQ';
42
+ | 'ILIKE' | 'STARTS_WITH' | 'ISTARTS_WITH' | 'ENDS_WITH' | 'IENDS_WITH' | 'IEQ'
43
+ | 'IS_EMPTY' | 'IS_NOT_EMPTY';
28
44
 
29
45
  /**
30
46
  * The query language features a {@link DatabaseAdapter} compiles. Declare it so that the runtime
@@ -36,6 +52,15 @@ export interface QueryFeatures {
36
52
  * version of Simfinity does not know are ignored.
37
53
  */
38
54
  operators: readonly (QueryOperator | (string & {}))[];
55
+ /**
56
+ * Whether the adapter compiles `IS_EMPTY` and `IS_NOT_EMPTY` through single references and at
57
+ * references and collections, including reference and collection members of embedded objects.
58
+ * Undefined or null means false; any other value that is not a boolean fails `createRuntime` with
59
+ * `INVALID_QUERY_FEATURES` (500). An adapter that declares it receives client constructs that carry
60
+ * a filled client scope marker, `__scope: { id, scopes }`, on their term or group condition, and
61
+ * must apply each target scope in `scopes`; constructs without a marker read stored data.
62
+ */
63
+ referenceEmptiness?: boolean | null | undefined;
39
64
  }
40
65
 
41
66
  export interface ModelRegistration {
@@ -279,8 +304,32 @@ export interface MutationLimitsOptions {
279
304
  maxNestedOperations?: number | null;
280
305
  }
281
306
 
307
+ /**
308
+ * The fourth `createSchema()` argument. Any other key, or a value that is not a plain object,
309
+ * throws `INVALID_SCHEMA_OPTIONS` (500) before models are prepared.
310
+ */
311
+ export interface CreateSchemaOptions {
312
+ /**
313
+ * Custom queries to include, by name. `null` or omission includes every registered custom query,
314
+ * `[]` includes none, and names that are not registered are ignored.
315
+ */
316
+ includedCustomQueries?: string[] | null;
317
+ }
318
+
319
+ export interface QueryLimitsOptions {
320
+ /** Positive safe integer; defaults to 1000. Unpaged lists use min(100, maxPageSize). */
321
+ maxPageSize?: number;
322
+ /**
323
+ * Positive safe integer; defaults to 10. The most distinct scoped joins and frames that the
324
+ * client's reference and collection emptiness may need in one list, aggregate or collection-field
325
+ * call; more fail with `FILTER_DEPTH_EXCEEDED` (400).
326
+ */
327
+ maxReferenceFrames?: number;
328
+ }
329
+
282
330
  export interface Runtime<Model = any, Session = any> {
283
- configureQueryLimits(options?: { maxPageSize?: number }): void;
331
+ /** The same process-wide setting as the root `configureQueryLimits`. */
332
+ configureQueryLimits(options?: QueryLimitsOptions): void;
284
333
  /**
285
334
  * Process-wide; calling it without options restores the unlimited default. Invalid options,
286
335
  * including unknown or misspelled option keys, throw `INVALID_MUTATION_LIMITS` (400) and keep
@@ -312,7 +361,7 @@ export interface Runtime<Model = any, Session = any> {
312
361
  * not virtual, and is not a reference or collection, logs a `Configuration issue` warning once. The
313
362
  * first schema binds the types whose relation resolvers it generates to this runtime and
314
363
  * reserves reachable types that still have a
315
- * non-embedded relation field without a resolver, such as an unregistered custom mutation result.
364
+ * non-embedded relation field without a resolver, such as an unregistered custom operation result.
316
365
  * Every entity type resolves `id` to `_id ?? id`, whatever the allowlists include.
317
366
  *
318
367
  * A generated input that would have no fields is left out, with a `Configuration issue`
@@ -326,12 +375,16 @@ export interface Runtime<Model = any, Session = any> {
326
375
  * metadata types `SimfinityFieldExtensionsType` and `SimfinityRelationType` (numbered when
327
376
  * taken) for every later schema and logs a warning; once those names are fixed, a type with one
328
377
  * of them throws `RESERVED_TYPE_NAME` (500), before `adapter.prepare()` when it is an included
329
- * endpoint type, a registered mutation's input or output type, an output type they reach or a
378
+ * endpoint type, a registered custom operation's input or output type, an output type they reach or a
330
379
  * field argument's type, and when the schema is constructed otherwise, such as for a type inside
331
380
  * an input object's fields. The check before `adapter.prepare()` never reads an input object's
332
381
  * fields, so a registered input's `fields` function may call `getInputType()`.
382
+ *
383
+ * `options.includedCustomQueries` selects custom queries by name (see {@link CreateSchemaOptions});
384
+ * an included custom query named like an included generated query throws `QUERY_NAME_CONFLICT`
385
+ * (500). A schema without mutation fields has no `Mutation` root.
333
386
  */
334
- createSchema(includedQueryTypes?: GraphQLObjectType[] | null, includedMutationTypes?: GraphQLObjectType[] | null, includedCustomMutations?: string[] | null): GraphQLSchema;
387
+ createSchema(includedQueryTypes?: GraphQLObjectType[] | null, includedMutationTypes?: GraphQLObjectType[] | null, includedCustomMutations?: string[] | null, options?: CreateSchemaOptions): GraphQLSchema;
335
388
  getModel(type: GraphQLObjectType | { name: string }): Model | null | undefined;
336
389
  getType(name: string | { name: string }): GraphQLObjectType | null | undefined;
337
390
  /**
@@ -350,6 +403,13 @@ export interface Runtime<Model = any, Session = any> {
350
403
  */
351
404
  use(middleware: (params: MiddlewareContext, next: () => Promise<void>) => void | Promise<void>): void;
352
405
  registerMutation(name: string, description: string, input: GraphQLInputObjectType | null | undefined, output: GraphQLOutputType, callback: (args: any, session: Session, context: any) => any): void;
406
+ /**
407
+ * Register a custom query on `RootQueryType`, for a result that is not a stored record. The
408
+ * callback receives the input and the request context; no transaction or session wraps it.
409
+ * Throws `INVALID_QUERY_REGISTRATION` (500) for invalid arguments and `QUERY_NAME_CONFLICT`
410
+ * (500) for a name already registered.
411
+ */
412
+ registerCustomQuery(name: string, description: string, input: GraphQLInputObjectType | null | undefined, output: GraphQLOutputType, callback: (input: any, context: any) => any): void;
353
413
  /** Owns the full workflow transaction unless an active caller session is supplied. */
354
414
  saveObject(typeName: string, args: Record<string, any>, session?: Session, context?: any): Promise<any>;
355
415
  preventCreatingCollection(prevent: boolean): void;
@@ -360,13 +420,16 @@ export interface DatabaseAdapter<Model = any, Session = any> {
360
420
  /**
361
421
  * Optional. The filter operators this adapter compiles. `createRuntime` reads it once and throws
362
422
  * `INVALID_QUERY_FEATURES` (500) unless it is undefined, null or an object whose `operators` is an
363
- * array of strings. Without it the adapter supports the operators of 3.7.0 (`EQ`, `NE`, `LT`,
364
- * `LTE`, `GT`, `GTE`, `BTW`, `IN`, `NIN` and `LIKE`). A list, aggregate or collection read, or a
423
+ * array of strings and whose `referenceEmptiness`, if present, is a boolean or null. Without it the
424
+ * adapter supports the operators of 3.7.0 (`EQ`, `NE`, `LT`, `LTE`, `GT`, `GTE`, `BTW`, `IN`, `NIN`
425
+ * and `LIKE`) and no reference emptiness. A list, aggregate or collection read, or a
365
426
  * by-ID read through a `get_by_id` scope, whose arguments use a `QLOperator` value the adapter
366
427
  * does not support fails with `UNSUPPORTED_QUERY` (400), before middleware runs for the client's
367
428
  * arguments and before `find`, `count`, `aggregate`, `findChildren` or `findChildrenBatch` for
368
429
  * conditions that middleware, scopes and joined scopes added. Operators outside `QLOperator` still
369
- * reach the adapter, which reports them as before.
430
+ * reach the adapter, which reports them as before. Declaring `IS_EMPTY` and `IS_NOT_EMPTY` means
431
+ * compiling their semantics on the record's own fields; emptiness through references and
432
+ * collections also needs `referenceEmptiness: true`, and fails with `UNSUPPORTED_QUERY` without it.
370
433
  */
371
434
  queryFeatures?: QueryFeatures | null | undefined;
372
435
  bind?(runtime: Pick<Runtime<Model, Session>, 'getModel' | 'getType' | 'getRegistrations'>): void;
@@ -486,8 +549,25 @@ export interface DatabaseAdapter<Model = any, Session = any> {
486
549
  }
487
550
  export function createRuntime<Model = any, Session = any>(adapter: DatabaseAdapter<Model, Session>): Runtime<Model, Session>;
488
551
 
489
- /** `operator` is a {@link QueryOperator}; the value of a text operator is a validated string. */
490
- export type QueryPredicate = { kind: 'predicate'; path: string[]; operator: string; value: any } | { kind: 'and' | 'or'; terms: QueryPredicate[] };
552
+ /**
553
+ * `operator` is a {@link QueryOperator}; the value of a text operator is a validated string. For
554
+ * `IS_EMPTY` and `IS_NOT_EMPTY` the value is null, and `path` may end at an embedded object, an
555
+ * embedded list or a scalar list as well as at a scalar; `IS_NOT_EMPTY` is the exact complement,
556
+ * which the compiler builds. With `referenceEmptiness: true`, their `path` may also cross single
557
+ * references and end at a reference or a collection.
558
+ *
559
+ * `scopes` exists only on such an emptiness predicate from a client, and only for a compiler that
560
+ * declares `referenceEmptiness: true`. It has one key per dotted path from the queried record: each
561
+ * single reference the path crosses (a scoped join, which admits the target only where the scope
562
+ * admits it), the end when it is a reference or a collection, and each reference and collection
563
+ * member of an embedded object (frames, which keep only the records the scope admits). Each value is
564
+ * the target's `find` scope as a predicate in the target's own frame, or null when the scope added
565
+ * nothing; after a scoped crossing, a null hop still correlates through that scoped join. Without
566
+ * `scopes` the predicate reads stored data, through plain joins.
567
+ */
568
+ export type QueryPredicate =
569
+ | { kind: 'predicate'; path: string[]; operator: string; value: any; scopes?: Record<string, QueryPredicate | null> }
570
+ | { kind: 'and' | 'or'; terms: QueryPredicate[] };
491
571
  export interface QueryPlan {
492
572
  entity: string;
493
573
  where: QueryPredicate | null;
@@ -502,11 +582,38 @@ export interface QueryPlan {
502
582
  * a string or contains U+0000. With `operators`, the names the caller compiles, any other
503
583
  * `QLOperator` value fails with `UNSUPPORTED_QUERY` (400); unknown operators fail with
504
584
  * `INVALID_FILTER_OPERATOR` (400) either way.
585
+ *
586
+ * An emptiness condition is checked in this order: its form, path and group depth
587
+ * (`MISSING_FILTER_PATH` for `IS_EMPTY` or `IS_NOT_EMPTY` on an object filter without terms or beside
588
+ * terms, `AMBIGUOUS_FILTER_FIELD`, `INVALID_FILTER_PATH`, `INVALID_FILTER_FIELD`,
589
+ * `FILTER_DEPTH_EXCEEDED`), then `operators` (`UNSUPPORTED_QUERY`), then `INVALID_FILTER_PATH` for a
590
+ * value inside an embedded list or a collection, `INVALID_FILTER_OPERATOR` for an embedded type that
591
+ * reaches itself through single embedded members, `INVALID_FILTER_VALUE` for a value that is not
592
+ * omitted or null, and `UNSUPPORTED_QUERY` for a reference or collection on the path or among the
593
+ * members. A term without a path (omitted, null or `""`) and a group condition without a path test
594
+ * the field itself. For other operators, a filter path of more than one segment that ends at an
595
+ * object fails with `INVALID_FILTER_PATH`, as on MongoDB; a bare object field keeps
596
+ * `MISSING_FILTER_PATH`.
597
+ *
598
+ * `referenceEmptiness` defaults to true when `operators` is omitted and to false when it is given.
599
+ * When it is false, an emptiness condition that crosses or ends at a reference or a collection, or
600
+ * reads a reference or collection member, fails with `UNSUPPORTED_QUERY` where the order above has
601
+ * `UNSUPPORTED_QUERY` for references; when it is true, such a condition plans with stored semantics,
602
+ * unless its term or group condition carries a client scope marker that the runtime filled, which
603
+ * becomes the predicate's `scopes`. A marker that is not filled for every path of the condition fails
604
+ * with `DETACHED_CLIENT_FILTER` (500).
505
605
  */
506
- export function createQueryPlan(models: ModelDescription, entityName: string, input?: Record<string, any>, options?: { mode?: QueryPlan['mode']; operators?: Iterable<string> }): QueryPlan;
606
+ export function createQueryPlan(models: ModelDescription, entityName: string, input?: Record<string, any>, options?: { mode?: QueryPlan['mode']; operators?: Iterable<string>; referenceEmptiness?: boolean }): QueryPlan;
507
607
  export function resolveModelPath(models: ModelDescription, entityName: string, path: string | string[]): FieldDescription[];
508
608
 
509
- export function configureQueryLimits(options?: { maxPageSize?: number }): void;
609
+ /**
610
+ * Process-wide. `maxPageSize` (default 1000) caps page sizes. `maxReferenceFrames` (default 10) caps,
611
+ * per list, aggregate or collection-field call, the distinct scoped joins and frames that a client's
612
+ * reference and collection emptiness needs; more fail with `FILTER_DEPTH_EXCEEDED` (400). Each must be
613
+ * a positive safe integer, or the call throws `INVALID_QUERY_LIMITS` (400) and keeps both limits; an
614
+ * omitted option takes its default again.
615
+ */
616
+ export function configureQueryLimits(options?: QueryLimitsOptions): void;
510
617
  /**
511
618
  * Same process-wide setting as `Runtime.configureMutationLimits`. Unknown or misspelled options
512
619
  * throw `INVALID_MUTATION_LIMITS` (400) and keep the current limit.