@simtlix/simfinity-core 3.5.2 → 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/README.md +8 -3
- package/package.json +1 -1
- package/src/auth/expressions.js +77 -16
- package/src/auth/index.js +44 -8
- package/src/auth/rules.js +54 -10
- package/src/auth/thenable.js +17 -0
- package/src/index.js +1 -0
- package/src/mutation-limits.js +24 -0
- package/src/relation-owners.js +5 -0
- package/src/runtime.js +293 -57
- package/src/scalars/factory.js +30 -10
- package/src/scalars/pattern.js +16 -0
- package/src/scalars.js +3 -2
- package/src/validators.js +4 -4
- package/types/index.d.ts +144 -7
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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,7 +365,11 @@ 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
|
-
/**
|
|
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(
|
|
@@ -264,24 +385,40 @@ export const auth: {
|
|
|
264
385
|
options?: AuthPluginOptions,
|
|
265
386
|
): (resolve: any, parent: any, args: any, ctx: any, info: any) => Promise<any>;
|
|
266
387
|
resolvePath(obj: any, pathOrFn: string | ((obj: any) => any)): any;
|
|
267
|
-
|
|
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;
|
|
268
394
|
/** Required roles must be nonempty strings; invalid configuration throws TypeError. */
|
|
269
|
-
requireRole(
|
|
395
|
+
requireRole(
|
|
396
|
+
role: string | string[],
|
|
397
|
+
options?: { userPath?: string | ((ctx: any) => unknown); rolePath?: string | ((user: any) => unknown) },
|
|
398
|
+
): AuthRuleFunction;
|
|
270
399
|
/** Exact array membership; only a standalone '*' claim grants all permissions. */
|
|
271
400
|
requirePermission(
|
|
272
401
|
permission: string | string[],
|
|
273
|
-
options?: { userPath?: string; permissionsPath?: string },
|
|
402
|
+
options?: { userPath?: string | ((ctx: any) => unknown); permissionsPath?: string | ((user: any) => unknown) },
|
|
274
403
|
): AuthRuleFunction;
|
|
275
404
|
/** Requires at least one rule function; throws TypeError otherwise. */
|
|
276
405
|
composeRules(...rules: AuthRuleFunction[]): AuthRuleFunction;
|
|
277
406
|
/** Requires at least one rule function; throws TypeError otherwise. */
|
|
278
407
|
anyRule(...rules: AuthRuleFunction[]): AuthRuleFunction;
|
|
279
408
|
/** Compares nonempty string, finite number, or MongoDB ObjectId identities; missing IDs deny. */
|
|
280
|
-
isOwner(
|
|
409
|
+
isOwner(
|
|
410
|
+
ownerField?: string | ((parent: any) => unknown),
|
|
411
|
+
userIdField?: string | ((user: any) => unknown),
|
|
412
|
+
options?: { userPath?: string | ((ctx: any) => unknown) },
|
|
413
|
+
): AuthRuleFunction;
|
|
281
414
|
createRule(predicate: AuthRuleFunction, errorMessage?: string, errorCode?: string): AuthRuleFunction;
|
|
282
415
|
allow(): AuthRuleFunction;
|
|
283
416
|
deny(message?: string): AuthRuleFunction;
|
|
284
|
-
/**
|
|
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
|
+
*/
|
|
285
422
|
evaluateExpression(expression: unknown, context: any): boolean;
|
|
286
423
|
isPolicyExpression(value: unknown): boolean;
|
|
287
424
|
/** Throws TypeError for a malformed expression. */
|