@simtlix/simfinity-core 3.5.2 → 3.5.5

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.
@@ -1,5 +1,18 @@
1
1
  import { GraphQLScalarType, GraphQLString, GraphQLInt, GraphQLFloat, GraphQLBoolean, GraphQLID, Kind } from 'graphql';
2
2
 
3
+ // The base each validated scalar delegates parsing to. A base exists before its scalar, so these links cannot cycle.
4
+ const validatedBases = new WeakMap();
5
+
6
+ // Follow validated-scalar chains to the scalar that defines the literal syntax. Any other scalar is the root, even
7
+ // with a hand-set `baseScalarType` storage hint, because its own parseLiteral decides which literals it takes.
8
+ const findRootScalar = (scalarType) => {
9
+ let root = scalarType;
10
+ while (validatedBases.has(root)) {
11
+ root = validatedBases.get(root);
12
+ }
13
+ return root;
14
+ };
15
+
3
16
  export function createValidatedScalar(name, description, baseScalarType, validate) {
4
17
  if (!baseScalarType) {
5
18
  throw new Error('baseScalarType is required');
@@ -16,16 +29,21 @@ export function createValidatedScalar(name, description, baseScalarType, validat
16
29
  throw new Error('baseScalarType must be a standard GraphQL scalar type or a custom scalar with a valid name');
17
30
  }
18
31
 
32
+ // Float takes integer literals too, as GraphQLFloat does, so an integral default printed as `5` stays valid.
19
33
  const kindMap = {
20
- String: Kind.STRING,
21
- Int: Kind.INT,
22
- Float: Kind.FLOAT,
23
- Boolean: Kind.BOOLEAN,
24
- ID: Kind.STRING,
34
+ String: [Kind.STRING],
35
+ Int: [Kind.INT],
36
+ Float: [Kind.FLOAT, Kind.INT],
37
+ Boolean: [Kind.BOOLEAN],
38
+ ID: [Kind.STRING],
25
39
  };
26
40
 
27
- const baseKind = kindMap[baseScalarType.name] || Kind.STRING;
41
+ // Custom root scalars define their own literal kinds, so their parseLiteral decides.
42
+ const rootScalarType = findRootScalar(baseScalarType);
43
+ const baseKinds = Object.hasOwn(kindMap, rootScalarType.name) ? kindMap[rootScalarType.name] : undefined;
28
44
 
45
+ // Input is parsed by the base first, so validators judge the same internal value for variables and literals.
46
+ // An undefined result is the base rejecting the input; GraphQL reports it without running the validator.
29
47
  const scalar = new GraphQLScalarType({
30
48
  name: `${name}_${baseScalarType.name}`,
31
49
  description,
@@ -34,19 +52,21 @@ export function createValidatedScalar(name, description, baseScalarType, validat
34
52
  return baseScalarType.serialize(value);
35
53
  },
36
54
  parseValue(value) {
37
- validate(value);
38
- return baseScalarType.parseValue(value);
55
+ const parsed = baseScalarType.parseValue(value);
56
+ if (parsed !== undefined) validate(parsed);
57
+ return parsed;
39
58
  },
40
59
  parseLiteral(ast, variables) {
41
- if (ast.kind !== baseKind) {
60
+ if (baseKinds !== undefined && !baseKinds.includes(ast.kind)) {
42
61
  throw new Error(`${name}_${baseScalarType.name} must be a ${baseScalarType.name}`);
43
62
  }
44
63
  const value = baseScalarType.parseLiteral(ast, variables);
45
- validate(value);
64
+ if (value !== undefined) validate(value);
46
65
  return value;
47
66
  },
48
67
  });
49
68
 
50
69
  scalar.baseScalarType = baseScalarType;
70
+ validatedBases.set(scalar, baseScalarType);
51
71
  return scalar;
52
72
  }
@@ -0,0 +1,16 @@
1
+ // A cross-realm RegExp fails `instanceof RegExp` but keeps this tag.
2
+ const isRegExp = (value) => Object.prototype.toString.call(value) === '[object RegExp]';
3
+
4
+ /** Private RegExp copy shared by `validators.pattern()` and `createPatternStringScalar()`; not a published subpath. */
5
+ export const compilePattern = (pattern) => {
6
+ if (typeof pattern !== 'string' && !isRegExp(pattern)) {
7
+ throw new TypeError('pattern must be a RegExp or a string');
8
+ }
9
+ return new RegExp(pattern);
10
+ };
11
+
12
+ /** Tests each value from its first character, so `g` and `y` keep no state between values. */
13
+ export const matchesPattern = (regex, value) => {
14
+ regex.lastIndex = 0;
15
+ return regex.test(value);
16
+ };
package/src/scalars.js CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  } from 'graphql';
4
4
  import { createValidatedScalar } from './scalars/factory.js';
5
5
  import { isEmailFormat } from './scalars/email.js';
6
+ import { compilePattern, matchesPattern } from './scalars/pattern.js';
6
7
 
7
8
  /**
8
9
  * Email scalar - validates email format
@@ -152,7 +153,7 @@ export const createBoundedFloatScalar = (name, min, max) => {
152
153
  * @returns {GraphQLScalarType} A scalar type with pattern validation
153
154
  */
154
155
  export const createPatternStringScalar = (name, pattern, message) => {
155
- const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
156
+ const regex = compilePattern(pattern);
156
157
  const errorMessage = message || 'Value does not match required pattern';
157
158
 
158
159
  return createValidatedScalar(
@@ -163,7 +164,7 @@ export const createPatternStringScalar = (name, pattern, message) => {
163
164
  if (typeof value !== 'string') {
164
165
  throw new Error('Value must be a string');
165
166
  }
166
- if (!regex.test(value)) {
167
+ if (!matchesPattern(regex, value)) {
167
168
  throw new Error(errorMessage);
168
169
  }
169
170
  },
package/src/validators.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import SimfinityError from './errors/simfinity.error.js';
2
2
  import { isEmailFormat } from './scalars/email.js';
3
+ import { compilePattern, matchesPattern } from './scalars/pattern.js';
3
4
 
4
5
  /**
5
6
  * Creates a validation object that works for both 'save' (CREATE) and 'update' (UPDATE) operations.
@@ -74,7 +75,7 @@ export const maxLength = (name, max) => {
74
75
  };
75
76
 
76
77
  export const pattern = (name, regex, message) => {
77
- const regexObj = typeof regex === 'string' ? new RegExp(regex) : regex;
78
+ const regexObj = compilePattern(regex);
78
79
  const errorMessage = message || `${name} format is invalid`;
79
80
 
80
81
  return createValidator(async (typeName, fieldName, value) => {
@@ -82,7 +83,7 @@ export const pattern = (name, regex, message) => {
82
83
  throw new SimfinityError(`${name} must be a string`, 'VALIDATION_ERROR', 400);
83
84
  }
84
85
 
85
- if (!regexObj.test(value)) {
86
+ if (!matchesPattern(regexObj, value)) {
86
87
  throw new SimfinityError(errorMessage, 'VALIDATION_ERROR', 400);
87
88
  }
88
89
  }, false); // Optional
@@ -109,8 +110,7 @@ export const url = () => {
109
110
  try {
110
111
  // Use URL constructor for better validation
111
112
  new URL(value);
112
- } catch (e) {
113
- console.log('Invalid URL format', e);
113
+ } catch {
114
114
  throw new SimfinityError('Invalid URL format', 'VALIDATION_ERROR', 400);
115
115
  }
116
116
  }, false); // Optional
package/types/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  GraphQLEnumType,
3
+ GraphQLError,
3
4
  GraphQLInputObjectType,
4
5
  GraphQLObjectType,
5
6
  GraphQLOutputType,
@@ -43,6 +44,16 @@ export interface ModelDescription {
43
44
  }>;
44
45
  }
45
46
  export function describeModels(registrations: ModelRegistration[]): ModelDescription;
47
+ /**
48
+ * Creates the scalar `${name}_${baseScalarType.name}`. `validate` throws to reject a value and
49
+ * receives the base scalar's internal value: the base's `parseValue` result for variables, the
50
+ * base's `parseLiteral` result for inline literals, and the resolver's value before serialization
51
+ * for output. Inline literal kinds are checked against the root of the chain of
52
+ * `createValidatedScalar` scalars when that root is String, ID, Int, Float or Boolean; a Float
53
+ * root accepts integer and float literals. Any other scalar is the root, even with a hand-set
54
+ * `baseScalarType` storage hint, and its own `parseLiteral` decides. `validate` is not called
55
+ * when the base rejects input by returning `undefined`.
56
+ */
46
57
  export function createValidatedScalar<TInternal, TExternal>(
47
58
  name: string,
48
59
  description: string,
@@ -55,6 +66,10 @@ export class SimfinityError extends Error {
55
66
  getCode(): string | undefined;
56
67
  getStatus(): number | undefined;
57
68
  getTimestamp(): string;
69
+ /** Set on errors that wrap a driver error, such as MongoDB's TRANSACTION_RETRY_EXCEEDED. */
70
+ cause?: unknown;
71
+ /** Set on errors that wrap a driver error, such as MongoDB's TRANSACTION_RETRY_EXCEEDED; returns `cause`. */
72
+ getCause?(): unknown;
58
73
  }
59
74
 
60
75
  export class InternalServerError extends SimfinityError {
@@ -62,7 +77,38 @@ export class InternalServerError extends SimfinityError {
62
77
  cause?: unknown;
63
78
  getCause(): unknown;
64
79
  }
65
- export function buildErrorFormatter(callback?: (error: SimfinityError) => Error | void): (error: Error) => Error;
80
+ /**
81
+ * Creates an error formatter for a GraphQL server's error hook. A field error is a `GraphQLError`
82
+ * with a `path`: for the one graphql-js creates around a value raised while resolving a field, such
83
+ * as one a resolver threw, it classifies that value, and a raised `GraphQLError` that already had a
84
+ * path is classified as a field error itself. A path-less `GraphQLError` is a request error, even
85
+ * when it wraps a plain Error with the same message. An explicit `InternalServerError` cause,
86
+ * followed through GraphQLErrors only, is kept for masking; mark unexpected subscription source
87
+ * failures with it because a path-less wrapper cannot distinguish them from scalar input errors.
88
+ * Any other input, such as a syntax, validation or variable error, is classified as is:
89
+ * - a `SimfinityError` is kept;
90
+ * - a request `GraphQLError`, or one raised while resolving a field that has its own string
91
+ * `extensions.code` or a `SimfinityError` cause, becomes a `SimfinityError` with its own message
92
+ * and extensions (including ones the server added, such as Yoga's `http`). Its code is that of a
93
+ * `SimfinityError` reached through `GraphQLError` causes only, else its string `extensions.code`
94
+ * (so a code the server set, such as Yoga's `GRAPHQL_PARSE_FAILED`, is kept), else
95
+ * `BAD_REQUEST`; its status is that error's, else its integer `extensions.status`, else 500 for
96
+ * `INTERNAL_SERVER_ERROR` and 400 otherwise. Its `originalError` is never exposed, so input
97
+ * rejected by Simfinity's validated scalars is `BAD_REQUEST` (400);
98
+ * - any other `GraphQLError` raised while resolving a field, including the ones graphql-js raises
99
+ * when it cannot complete a resolved value (scalar or enum serialization, `isTypeOf`, abstract
100
+ * type resolution, a non-iterable list), becomes `InternalServerError` with that message and the
101
+ * error as its cause;
102
+ * - any other `Error` becomes `InternalServerError` with that error as its cause;
103
+ * - a non-Error value becomes `InternalServerError('Unexpected error value')` with no cause. Servers
104
+ * whose executor first turns it into an `Error`, such as Yoga, pass the value's text instead.
105
+ *
106
+ * `callback` receives the classified error; check `error instanceof InternalServerError` to mask
107
+ * unexpected errors. A returned error replaces it, and a returned `GraphQLError` is returned
108
+ * unchanged. The result is a `GraphQLError` that keeps the input's locations and path, has the
109
+ * chosen error as `originalError`, and copies its `extensions`.
110
+ */
111
+ export function buildErrorFormatter(callback?: (error: SimfinityError) => Error | void): (error: unknown) => GraphQLError;
66
112
 
67
113
  export interface EntityController<Session = any> {
68
114
  onSaving?(record: any, args: any, session: Session | undefined, context: any): void | Promise<void>;
@@ -87,6 +133,14 @@ export interface RuntimeRegistration<Model = any, Session = any> extends ModelRe
87
133
  controller?: EntityController<Session> | null;
88
134
  onModelCreated?: ((model: Model) => void) | null;
89
135
  stateMachine?: StateMachine<Session> | null;
136
+ /**
137
+ * Set by `createSchema()` before `adapter.prepare()`: true for entity types, those registered
138
+ * with `connect()` and the targets of non-embedded relations, whose `id` resolves to the stored
139
+ * `_id`, falling back to `id`. False for types this runtime uses only as embedded values. The `id`
140
+ * rule follows the type object, so a type that another runtime sharing it treats as an entity
141
+ * resolves `id` the same way here.
142
+ */
143
+ storedIdentity?: boolean;
90
144
  }
91
145
  export interface MiddlewareContext {
92
146
  args: any;
@@ -96,16 +150,86 @@ export interface MiddlewareContext {
96
150
  context?: any;
97
151
  [key: string]: any;
98
152
  }
153
+ /** Operations that a type's `extensions.scope` can restrict. */
154
+ export type ScopeOperation = 'find' | 'get_by_id' | 'aggregate';
155
+ /** Argument of a scope function. Mutate `args` in place; the return value is ignored. */
156
+ export interface ScopeParams<Model = any, Session = any> {
157
+ type: RuntimeRegistration<Model, Session>;
158
+ args: Record<string, any>;
159
+ operation: ScopeOperation;
160
+ context: any;
161
+ }
162
+ export type ScopeFunction<Model = any, Session = any> = (params: ScopeParams<Model, Session>) => unknown;
163
+ /**
164
+ * Value of a type's `extensions.scope`: a plain object whose keys are scope operations and whose
165
+ * values are functions. Omit a key to leave that operation unscoped. A present key must hold a
166
+ * function: `{ find: undefined }` type-checks but throws `INVALID_SCOPE` (500) at startup, so
167
+ * leave the key out instead. Every own string key is checked, including non-enumerable ones. Any
168
+ * other shape throws `INVALID_SCOPE` at registration, at `createSchema()`, or on a read after it
169
+ * was changed.
170
+ */
171
+ export type TypeScopes<Model = any, Session = any> = Partial<Record<ScopeOperation, ScopeFunction<Model, Session>>>;
172
+
173
+ /**
174
+ * Process-wide limits for generated add, update and state-action mutations. Any other option,
175
+ * such as a misspelled `maxNestedOperation`, throws `INVALID_MUTATION_LIMITS` (400).
176
+ */
177
+ export interface MutationLimitsOptions {
178
+ /**
179
+ * Maximum number of `added`, `updated` and `deleted` entries across every nested level of
180
+ * non-embedded collection fields in one generated mutation. A non-negative safe integer;
181
+ * `null` or omitted means unlimited (the default).
182
+ */
183
+ maxNestedOperations?: number | null;
184
+ }
185
+
99
186
  export interface Runtime<Model = any, Session = any> {
100
187
  configureQueryLimits(options?: { maxPageSize?: number }): void;
188
+ /**
189
+ * Process-wide; calling it without options restores the unlimited default. Invalid options,
190
+ * including unknown or misspelled option keys, throw `INVALID_MUTATION_LIMITS` (400) and keep
191
+ * the current limit. A generated mutation over the limit fails with
192
+ * `NESTED_OPERATIONS_EXCEEDED` (400) before its transaction starts.
193
+ */
194
+ configureMutationLimits(options?: MutationLimitsOptions): void;
195
+ /**
196
+ * Throws `INVALID_SCOPE` (500) for an invalid `extensions.scope`, and
197
+ * `TYPE_BOUND_TO_OTHER_RUNTIME` (409) when another runtime bound the type by generating its
198
+ * relation resolvers, or reserved it because its schema reached the type with an unresolved
199
+ * relation field. A rejected type is not registered.
200
+ */
101
201
  connect(model: Model | null, type: GraphQLObjectType, singular: string, plural: string, controller?: EntityController<Session> | null, onModelCreated?: ((model: Model) => void) | null, stateMachine?: StateMachine<Session> | null): void;
202
+ /** Throws like `connect` for an invalid scope or a type bound to another runtime. */
102
203
  addNoEndpointType(type: GraphQLObjectType): void;
204
+ /**
205
+ * Validates every registered scope (`INVALID_SCOPE`) before building models, and rejects
206
+ * (`TYPE_BOUND_TO_OTHER_RUNTIME`) any reachable type that another runtime bound or reserved, or
207
+ * whose field was copied with `toConfig()` after another runtime generated its relation
208
+ * resolver, keeping the generated resolver or its extensions. A list or embedded relation to an unregistered type throws
209
+ * `UNREGISTERED_RELATION_TARGET` (500). A writable non-embedded list relation without a
210
+ * non-empty child `connectionField`, whose resolver this runtime would generate, throws
211
+ * `INVALID_MODEL` (400) before `adapter.prepare()`; one with an application resolver or
212
+ * `readOnly` only logs a `Configuration issue` warning, and its nested writes fail with
213
+ * `INVALID_MODEL` (500). Fields named like generated query arguments also log a warning. The
214
+ * first schema binds the types whose relation resolvers it generates to this runtime and
215
+ * reserves reachable types that still have a
216
+ * non-embedded relation field without a resolver, such as an unregistered custom mutation result.
217
+ * Every entity type resolves `id` to `_id ?? id`, whatever the allowlists include.
218
+ */
103
219
  createSchema(includedQueryTypes?: GraphQLObjectType[] | null, includedMutationTypes?: GraphQLObjectType[] | null, includedCustomMutations?: string[] | null): GraphQLSchema;
104
220
  getModel(type: GraphQLObjectType | { name: string }): Model | null | undefined;
105
221
  getType(name: string | { name: string }): GraphQLObjectType | null | undefined;
106
222
  /** Available after createSchema has built input types. */
107
223
  getInputType(type: GraphQLObjectType | { name: string }): GraphQLInputObjectType | undefined;
108
224
  getRegistrations(): RuntimeRegistration<Model, Session>[];
225
+ /**
226
+ * Middleware runs in registration order before the operation. Throws `INVALID_MIDDLEWARE`
227
+ * (500) for a non-function. The rest of the chain runs at most once and is awaited even when a
228
+ * middleware does not await `next()`; its errors cancel the operation, even if a middleware
229
+ * catches them. Call `next()` before the middleware returns or its promise settles: a later
230
+ * call, such as `setTimeout(next)`, does nothing. Omitting `next()` skips the remaining
231
+ * middleware only.
232
+ */
109
233
  use(middleware: (params: MiddlewareContext, next: () => Promise<void>) => void | Promise<void>): void;
110
234
  registerMutation(name: string, description: string, input: GraphQLInputObjectType | null | undefined, output: GraphQLOutputType, callback: (args: any, session: Session, context: any) => any): void;
111
235
  /** Owns the full workflow transaction unless an active caller session is supplied. */
@@ -119,12 +243,29 @@ export interface DatabaseAdapter<Model = any, Session = any> {
119
243
  validateRegistration?(registration: RuntimeRegistration<Model, Session>): void;
120
244
  prepare?(registrations: RuntimeRegistration<Model, Session>[], options: { createCollection: boolean }): void;
121
245
  createModel(type: GraphQLObjectType, callback: ((model: Model) => void) | null | undefined, options: { createCollection: boolean }): Model;
246
+ /**
247
+ * Casts a reference or batch key to the stored identifier type. Throw a `SimfinityError` with
248
+ * code `NOT_VALID_ID` (400) for a malformed value; never create a new identifier. A batched
249
+ * reference read whose ID throws or is normalized reads that ID alone with `getById`. When
250
+ * `getById` rejects a stored reference with that error, the generated reference field fails with
251
+ * an `InternalServerError` whose cause is that error, since the client did not send the value.
252
+ */
122
253
  castId(value: any): any;
123
254
  stateValue?(state: { name: string; value: any }): any;
124
255
  withTransaction<T>(session: Session | null | undefined, callback: (session: Session) => Promise<T> | T, model?: Model): Promise<T>;
125
256
  newRecord(model: Model, data: any, session?: Session): any;
126
257
  saveRecord(model: Model, record: any, session?: Session): any;
127
258
  toObject(record: any): any;
259
+ /**
260
+ * Optional. When defined, every nullable, singular embedded object field without a resolver
261
+ * passes its value through `readEmbeddedValue(value)`. The field is read as graphql's default
262
+ * resolver reads it: a method on the parent is called with (args, context, info), and the hook
263
+ * receives its result once a returned promise settles. Return null for a value that a hydrated
264
+ * record renders as an object although the stored value is an explicit null; return any other
265
+ * value unchanged. It must depend only on the value: these resolvers read no data and do not bind
266
+ * their types, so a runtime that reaches a shared type may read through another runtime's hook.
267
+ */
268
+ readEmbeddedValue?(value: any): any;
128
269
  getById(model: Model, id: any, session?: Session | null, options?: { projection?: Record<string, number>; plain?: boolean; lock?: boolean; requiredId?: any; context?: any }): any;
129
270
  /** Optional batch read: the records found for `ids`, in getById shape and any order. After a failure, the runtime reads each ID with getById. */
130
271
  getByIds?(model: Model, ids: any[], options?: { context?: any }): any;
@@ -151,6 +292,11 @@ export function createQueryPlan(models: ModelDescription, entityName: string, in
151
292
  export function resolveModelPath(models: ModelDescription, entityName: string, path: string | string[]): FieldDescription[];
152
293
 
153
294
  export function configureQueryLimits(options?: { maxPageSize?: number }): void;
295
+ /**
296
+ * Same process-wide setting as `Runtime.configureMutationLimits`. Unknown or misspelled options
297
+ * throw `INVALID_MUTATION_LIMITS` (400) and keep the current limit.
298
+ */
299
+ export function configureMutationLimits(options?: MutationLimitsOptions): void;
154
300
  export function paginationStages(pagination: { page: number; size: number } | null | undefined, withDefault: boolean): Array<{ $skip: number } | { $limit: number }>;
155
301
 
156
302
  /** Envelop-style schema plugin used by the shared authorization helpers. */
@@ -181,6 +327,11 @@ export interface FieldValidations {
181
327
  export const validators: {
182
328
  stringLength(name: string, min?: number, max?: number): FieldValidations;
183
329
  maxLength(name: string, max: number): FieldValidations;
330
+ /**
331
+ * Copies the pattern when the helper is created and tests each value from its first character,
332
+ * so `g` and `y` keep no state between values. Throws `TypeError` unless the pattern is a
333
+ * `RegExp` or a string.
334
+ */
184
335
  pattern(name: string, regex: RegExp | string, message?: string): FieldValidations;
185
336
  email(): FieldValidations;
186
337
  url(): FieldValidations;
@@ -204,6 +355,7 @@ export const scalars: {
204
355
  createBoundedStringScalar(name: string, min?: number, max?: number): GraphQLScalarType;
205
356
  createBoundedIntScalar(name: string, min?: number, max?: number): GraphQLScalarType;
206
357
  createBoundedFloatScalar(name: string, min?: number, max?: number): GraphQLScalarType;
358
+ /** Same pattern rules as `validators.pattern`: a private copy, tested from the first character. */
207
359
  createPatternStringScalar(name: string, pattern: RegExp | string, message?: string): GraphQLScalarType;
208
360
  };
209
361
 
@@ -248,7 +400,11 @@ declare class ForbiddenError extends SimfinityError {
248
400
 
249
401
  /** Authorization utilities (RBAC/ABAC rules, plugin factories and auth errors). */
250
402
  export const auth: {
251
- /** Wraps schema resolvers in-place. Throws TypeError for invalid rules, maps, or defaultPolicy. */
403
+ /**
404
+ * Wraps schema resolvers in-place. Throws TypeError for invalid rules, maps, or defaultPolicy.
405
+ * `onSchemaChange` throws TypeError, before wrapping any field, when a schema type was created
406
+ * by another copy of the graphql module.
407
+ */
252
408
  createAuthPlugin(permissions: PermissionSchema, options?: AuthPluginOptions): EnvelopSchemaPlugin;
253
409
  /** @deprecated Use createAuthPlugin instead. graphql-middleware compatible middleware. */
254
410
  createAuthMiddleware(
@@ -264,24 +420,40 @@ export const auth: {
264
420
  options?: AuthPluginOptions,
265
421
  ): (resolve: any, parent: any, args: any, ctx: any, info: any) => Promise<any>;
266
422
  resolvePath(obj: any, pathOrFn: string | ((obj: any) => any)): any;
267
- requireAuth(userPath?: string): AuthRuleFunction;
423
+ /**
424
+ * Paths in the helpers below are dotted strings or synchronous extractors. An `async`
425
+ * extractor throws TypeError when the helper is created; a path that yields a promise or other
426
+ * thenable denies with TypeError.
427
+ */
428
+ requireAuth(userPath?: string | ((ctx: any) => unknown)): AuthRuleFunction;
268
429
  /** Required roles must be nonempty strings; invalid configuration throws TypeError. */
269
- requireRole(role: string | string[], options?: { userPath?: string; rolePath?: string }): AuthRuleFunction;
430
+ requireRole(
431
+ role: string | string[],
432
+ options?: { userPath?: string | ((ctx: any) => unknown); rolePath?: string | ((user: any) => unknown) },
433
+ ): AuthRuleFunction;
270
434
  /** Exact array membership; only a standalone '*' claim grants all permissions. */
271
435
  requirePermission(
272
436
  permission: string | string[],
273
- options?: { userPath?: string; permissionsPath?: string },
437
+ options?: { userPath?: string | ((ctx: any) => unknown); permissionsPath?: string | ((user: any) => unknown) },
274
438
  ): AuthRuleFunction;
275
439
  /** Requires at least one rule function; throws TypeError otherwise. */
276
440
  composeRules(...rules: AuthRuleFunction[]): AuthRuleFunction;
277
441
  /** Requires at least one rule function; throws TypeError otherwise. */
278
442
  anyRule(...rules: AuthRuleFunction[]): AuthRuleFunction;
279
443
  /** Compares nonempty string, finite number, or MongoDB ObjectId identities; missing IDs deny. */
280
- isOwner(ownerField?: string, userIdField?: string, options?: { userPath?: string }): AuthRuleFunction;
444
+ isOwner(
445
+ ownerField?: string | ((parent: any) => unknown),
446
+ userIdField?: string | ((user: any) => unknown),
447
+ options?: { userPath?: string | ((ctx: any) => unknown) },
448
+ ): AuthRuleFunction;
281
449
  createRule(predicate: AuthRuleFunction, errorMessage?: string, errorCode?: string): AuthRuleFunction;
282
450
  allow(): AuthRuleFunction;
283
451
  deny(message?: string): AuthRuleFunction;
284
- /** Invalid ASTs and unresolved comparisons deny, including under negation. */
452
+ /**
453
+ * Invalid ASTs and unresolved comparisons deny, including under negation. Dates compare by
454
+ * time. Promise operands are always invalid; NaN and Date/non-Date comparisons are invalid
455
+ * except against `null`, which stays comparable with any value (the comparison is false).
456
+ */
285
457
  evaluateExpression(expression: unknown, context: any): boolean;
286
458
  isPolicyExpression(value: unknown): boolean;
287
459
  /** Throws TypeError for a malformed expression. */