@simtlix/simfinity-core 3.5.1 → 3.5.3

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/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,
@@ -62,7 +73,38 @@ export class InternalServerError extends SimfinityError {
62
73
  cause?: unknown;
63
74
  getCause(): unknown;
64
75
  }
65
- export function buildErrorFormatter(callback?: (error: SimfinityError) => Error | void): (error: Error) => Error;
76
+ /**
77
+ * Creates an error formatter for a GraphQL server's error hook. A field error is a `GraphQLError`
78
+ * with a `path`: for the one graphql-js creates around a value raised while resolving a field, such
79
+ * as one a resolver threw, it classifies that value, and a raised `GraphQLError` that already had a
80
+ * path is classified as a field error itself. A path-less `GraphQLError` is a request error, even
81
+ * when it wraps a plain Error with the same message. An explicit `InternalServerError` cause,
82
+ * followed through GraphQLErrors only, is kept for masking; mark unexpected subscription source
83
+ * failures with it because a path-less wrapper cannot distinguish them from scalar input errors.
84
+ * Any other input, such as a syntax, validation or variable error, is classified as is:
85
+ * - a `SimfinityError` is kept;
86
+ * - a request `GraphQLError`, or one raised while resolving a field that has its own string
87
+ * `extensions.code` or a `SimfinityError` cause, becomes a `SimfinityError` with its own message
88
+ * and extensions (including ones the server added, such as Yoga's `http`). Its code is that of a
89
+ * `SimfinityError` reached through `GraphQLError` causes only, else its string `extensions.code`
90
+ * (so a code the server set, such as Yoga's `GRAPHQL_PARSE_FAILED`, is kept), else
91
+ * `BAD_REQUEST`; its status is that error's, else its integer `extensions.status`, else 500 for
92
+ * `INTERNAL_SERVER_ERROR` and 400 otherwise. Its `originalError` is never exposed, so input
93
+ * rejected by Simfinity's validated scalars is `BAD_REQUEST` (400);
94
+ * - any other `GraphQLError` raised while resolving a field, including the ones graphql-js raises
95
+ * when it cannot complete a resolved value (scalar or enum serialization, `isTypeOf`, abstract
96
+ * type resolution, a non-iterable list), becomes `InternalServerError` with that message and the
97
+ * error as its cause;
98
+ * - any other `Error` becomes `InternalServerError` with that error as its cause;
99
+ * - a non-Error value becomes `InternalServerError('Unexpected error value')` with no cause. Servers
100
+ * whose executor first turns it into an `Error`, such as Yoga, pass the value's text instead.
101
+ *
102
+ * `callback` receives the classified error; check `error instanceof InternalServerError` to mask
103
+ * unexpected errors. A returned error replaces it, and a returned `GraphQLError` is returned
104
+ * unchanged. The result is a `GraphQLError` that keeps the input's locations and path, has the
105
+ * chosen error as `originalError`, and copies its `extensions`.
106
+ */
107
+ export function buildErrorFormatter(callback?: (error: SimfinityError) => Error | void): (error: unknown) => GraphQLError;
66
108
 
67
109
  export interface EntityController<Session = any> {
68
110
  onSaving?(record: any, args: any, session: Session | undefined, context: any): void | Promise<void>;
@@ -96,16 +138,80 @@ export interface MiddlewareContext {
96
138
  context?: any;
97
139
  [key: string]: any;
98
140
  }
141
+ /** Operations that a type's `extensions.scope` can restrict. */
142
+ export type ScopeOperation = 'find' | 'get_by_id' | 'aggregate';
143
+ /** Argument of a scope function. Mutate `args` in place; the return value is ignored. */
144
+ export interface ScopeParams<Model = any, Session = any> {
145
+ type: RuntimeRegistration<Model, Session>;
146
+ args: Record<string, any>;
147
+ operation: ScopeOperation;
148
+ context: any;
149
+ }
150
+ export type ScopeFunction<Model = any, Session = any> = (params: ScopeParams<Model, Session>) => unknown;
151
+ /**
152
+ * Value of a type's `extensions.scope`: a plain object whose keys are scope operations and whose
153
+ * values are functions. Omit a key to leave that operation unscoped. A present key must hold a
154
+ * function: `{ find: undefined }` type-checks but throws `INVALID_SCOPE` (500) at startup, so
155
+ * leave the key out instead. Every own string key is checked, including non-enumerable ones. Any
156
+ * other shape throws `INVALID_SCOPE` at registration, at `createSchema()`, or on a read after it
157
+ * was changed.
158
+ */
159
+ export type TypeScopes<Model = any, Session = any> = Partial<Record<ScopeOperation, ScopeFunction<Model, Session>>>;
160
+
161
+ /**
162
+ * Process-wide limits for generated add, update and state-action mutations. Any other option,
163
+ * such as a misspelled `maxNestedOperation`, throws `INVALID_MUTATION_LIMITS` (400).
164
+ */
165
+ export interface MutationLimitsOptions {
166
+ /**
167
+ * Maximum number of `added`, `updated` and `deleted` entries across every nested level of
168
+ * non-embedded collection fields in one generated mutation. A non-negative safe integer;
169
+ * `null` or omitted means unlimited (the default).
170
+ */
171
+ maxNestedOperations?: number | null;
172
+ }
173
+
99
174
  export interface Runtime<Model = any, Session = any> {
100
175
  configureQueryLimits(options?: { maxPageSize?: number }): void;
176
+ /**
177
+ * Process-wide; calling it without options restores the unlimited default. Invalid options,
178
+ * including unknown or misspelled option keys, throw `INVALID_MUTATION_LIMITS` (400) and keep
179
+ * the current limit. A generated mutation over the limit fails with
180
+ * `NESTED_OPERATIONS_EXCEEDED` (400) before its transaction starts.
181
+ */
182
+ configureMutationLimits(options?: MutationLimitsOptions): void;
183
+ /**
184
+ * Throws `INVALID_SCOPE` (500) for an invalid `extensions.scope`, and
185
+ * `TYPE_BOUND_TO_OTHER_RUNTIME` (409) when another runtime bound the type by generating its
186
+ * relation resolvers, or reserved it because its schema reached the type with an unresolved
187
+ * relation field. A rejected type is not registered.
188
+ */
101
189
  connect(model: Model | null, type: GraphQLObjectType, singular: string, plural: string, controller?: EntityController<Session> | null, onModelCreated?: ((model: Model) => void) | null, stateMachine?: StateMachine<Session> | null): void;
190
+ /** Throws like `connect` for an invalid scope or a type bound to another runtime. */
102
191
  addNoEndpointType(type: GraphQLObjectType): void;
192
+ /**
193
+ * Validates every registered scope (`INVALID_SCOPE`) before building models, and rejects
194
+ * (`TYPE_BOUND_TO_OTHER_RUNTIME`) any reachable type that another runtime bound or reserved, or
195
+ * whose field was copied with `toConfig()` after another runtime generated its relation
196
+ * resolver, keeping the generated resolver or its extensions. A list or embedded relation to an unregistered type throws
197
+ * `UNREGISTERED_RELATION_TARGET` (500). The first schema binds the types whose relation
198
+ * resolvers it generates to this runtime, and reserves reachable types that still have a
199
+ * non-embedded relation field without a resolver, such as an unregistered custom mutation result.
200
+ */
103
201
  createSchema(includedQueryTypes?: GraphQLObjectType[] | null, includedMutationTypes?: GraphQLObjectType[] | null, includedCustomMutations?: string[] | null): GraphQLSchema;
104
202
  getModel(type: GraphQLObjectType | { name: string }): Model | null | undefined;
105
203
  getType(name: string | { name: string }): GraphQLObjectType | null | undefined;
106
204
  /** Available after createSchema has built input types. */
107
205
  getInputType(type: GraphQLObjectType | { name: string }): GraphQLInputObjectType | undefined;
108
206
  getRegistrations(): RuntimeRegistration<Model, Session>[];
207
+ /**
208
+ * Middleware runs in registration order before the operation. Throws `INVALID_MIDDLEWARE`
209
+ * (500) for a non-function. The rest of the chain runs at most once and is awaited even when a
210
+ * middleware does not await `next()`; its errors cancel the operation, even if a middleware
211
+ * catches them. Call `next()` before the middleware returns or its promise settles: a later
212
+ * call, such as `setTimeout(next)`, does nothing. Omitting `next()` skips the remaining
213
+ * middleware only.
214
+ */
109
215
  use(middleware: (params: MiddlewareContext, next: () => Promise<void>) => void | Promise<void>): void;
110
216
  registerMutation(name: string, description: string, input: GraphQLInputObjectType | null | undefined, output: GraphQLOutputType, callback: (args: any, session: Session, context: any) => any): void;
111
217
  /** Owns the full workflow transaction unless an active caller session is supplied. */
@@ -151,6 +257,11 @@ export function createQueryPlan(models: ModelDescription, entityName: string, in
151
257
  export function resolveModelPath(models: ModelDescription, entityName: string, path: string | string[]): FieldDescription[];
152
258
 
153
259
  export function configureQueryLimits(options?: { maxPageSize?: number }): void;
260
+ /**
261
+ * Same process-wide setting as `Runtime.configureMutationLimits`. Unknown or misspelled options
262
+ * throw `INVALID_MUTATION_LIMITS` (400) and keep the current limit.
263
+ */
264
+ export function configureMutationLimits(options?: MutationLimitsOptions): void;
154
265
  export function paginationStages(pagination: { page: number; size: number } | null | undefined, withDefault: boolean): Array<{ $skip: number } | { $limit: number }>;
155
266
 
156
267
  /** Envelop-style schema plugin used by the shared authorization helpers. */
@@ -181,6 +292,11 @@ export interface FieldValidations {
181
292
  export const validators: {
182
293
  stringLength(name: string, min?: number, max?: number): FieldValidations;
183
294
  maxLength(name: string, max: number): FieldValidations;
295
+ /**
296
+ * Copies the pattern when the helper is created and tests each value from its first character,
297
+ * so `g` and `y` keep no state between values. Throws `TypeError` unless the pattern is a
298
+ * `RegExp` or a string.
299
+ */
184
300
  pattern(name: string, regex: RegExp | string, message?: string): FieldValidations;
185
301
  email(): FieldValidations;
186
302
  url(): FieldValidations;
@@ -204,6 +320,7 @@ export const scalars: {
204
320
  createBoundedStringScalar(name: string, min?: number, max?: number): GraphQLScalarType;
205
321
  createBoundedIntScalar(name: string, min?: number, max?: number): GraphQLScalarType;
206
322
  createBoundedFloatScalar(name: string, min?: number, max?: number): GraphQLScalarType;
323
+ /** Same pattern rules as `validators.pattern`: a private copy, tested from the first character. */
207
324
  createPatternStringScalar(name: string, pattern: RegExp | string, message?: string): GraphQLScalarType;
208
325
  };
209
326
 
@@ -248,35 +365,60 @@ declare class ForbiddenError extends SimfinityError {
248
365
 
249
366
  /** Authorization utilities (RBAC/ABAC rules, plugin factories and auth errors). */
250
367
  export const auth: {
251
- /** Wraps schema resolvers in-place. Throws TypeError for invalid rules, maps, or defaultPolicy. */
368
+ /**
369
+ * Wraps schema resolvers in-place. Throws TypeError for invalid rules, maps, or defaultPolicy.
370
+ * `onSchemaChange` throws TypeError, before wrapping any field, when a schema type was created
371
+ * by another copy of the graphql module.
372
+ */
252
373
  createAuthPlugin(permissions: PermissionSchema, options?: AuthPluginOptions): EnvelopSchemaPlugin;
253
374
  /** @deprecated Use createAuthPlugin instead. graphql-middleware compatible middleware. */
254
375
  createAuthMiddleware(
255
376
  permissions: PermissionSchema,
256
377
  options?: AuthPluginOptions,
257
378
  ): (resolve: any, parent: any, args: any, ctx: any, info: any) => Promise<any>;
258
- /** @deprecated Use createAuthPlugin instead. Field middleware object for graphql-middleware. */
379
+ /**
380
+ * @deprecated Use createAuthPlugin instead. The same graphql-middleware function as
381
+ * createAuthMiddleware, which applies wildcard rules and the default policy to every field.
382
+ */
259
383
  createFieldMiddleware(
260
384
  permissions: PermissionSchema,
261
385
  options?: AuthPluginOptions,
262
- ): Record<string, Record<string, any>>;
386
+ ): (resolve: any, parent: any, args: any, ctx: any, info: any) => Promise<any>;
263
387
  resolvePath(obj: any, pathOrFn: string | ((obj: any) => any)): any;
264
- requireAuth(userPath?: string): AuthRuleFunction;
388
+ /**
389
+ * Paths in the helpers below are dotted strings or synchronous extractors. An `async`
390
+ * extractor throws TypeError when the helper is created; a path that yields a promise or other
391
+ * thenable denies with TypeError.
392
+ */
393
+ requireAuth(userPath?: string | ((ctx: any) => unknown)): AuthRuleFunction;
265
394
  /** Required roles must be nonempty strings; invalid configuration throws TypeError. */
266
- requireRole(role: string | string[], options?: { userPath?: string; rolePath?: string }): AuthRuleFunction;
395
+ requireRole(
396
+ role: string | string[],
397
+ options?: { userPath?: string | ((ctx: any) => unknown); rolePath?: string | ((user: any) => unknown) },
398
+ ): AuthRuleFunction;
267
399
  /** Exact array membership; only a standalone '*' claim grants all permissions. */
268
400
  requirePermission(
269
401
  permission: string | string[],
270
- options?: { userPath?: string; permissionsPath?: string },
402
+ options?: { userPath?: string | ((ctx: any) => unknown); permissionsPath?: string | ((user: any) => unknown) },
271
403
  ): AuthRuleFunction;
404
+ /** Requires at least one rule function; throws TypeError otherwise. */
272
405
  composeRules(...rules: AuthRuleFunction[]): AuthRuleFunction;
406
+ /** Requires at least one rule function; throws TypeError otherwise. */
273
407
  anyRule(...rules: AuthRuleFunction[]): AuthRuleFunction;
274
408
  /** Compares nonempty string, finite number, or MongoDB ObjectId identities; missing IDs deny. */
275
- isOwner(ownerField?: string, userIdField?: string, options?: { userPath?: string }): AuthRuleFunction;
409
+ isOwner(
410
+ ownerField?: string | ((parent: any) => unknown),
411
+ userIdField?: string | ((user: any) => unknown),
412
+ options?: { userPath?: string | ((ctx: any) => unknown) },
413
+ ): AuthRuleFunction;
276
414
  createRule(predicate: AuthRuleFunction, errorMessage?: string, errorCode?: string): AuthRuleFunction;
277
415
  allow(): AuthRuleFunction;
278
416
  deny(message?: string): AuthRuleFunction;
279
- /** Invalid ASTs and unresolved comparisons deny, including under negation. */
417
+ /**
418
+ * Invalid ASTs and unresolved comparisons deny, including under negation. Dates compare by
419
+ * time. Promise operands are always invalid; NaN and Date/non-Date comparisons are invalid
420
+ * except against `null`, which stays comparable with any value (the comparison is false).
421
+ */
280
422
  evaluateExpression(expression: unknown, context: any): boolean;
281
423
  isPolicyExpression(value: unknown): boolean;
282
424
  /** Throws TypeError for a malformed expression. */