@simtlix/simfinity-core 3.5.7 → 3.5.8

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/scalars.js CHANGED
@@ -67,26 +67,56 @@ export const PositiveFloatScalar = createValidatedScalar(
67
67
  },
68
68
  );
69
69
 
70
+ // A null or omitted bound means no bound (#97). Other values keep JavaScript's comparison, so numeric strings work.
71
+ const hasBound = (bound) => bound !== undefined && bound !== null;
72
+
73
+ // The published description names only the bounds that are set. A NaN bound, which no comparison applies, is left out
74
+ // too. With both bounds the text is the same as in earlier versions.
75
+ const describeBounds = (min, max, texts) => {
76
+ const described = (bound) => hasBound(bound) && !(typeof bound === 'number' && Number.isNaN(bound));
77
+ if (described(min) && described(max)) return texts.between;
78
+ if (described(min)) return texts.atLeast;
79
+ if (described(max)) return texts.atMost;
80
+ return texts.unbounded;
81
+ };
82
+
83
+ const checkNumberBounds = (min, max) => (value) => {
84
+ if (typeof value !== 'number' || isNaN(value)) {
85
+ throw new Error('Value must be a number');
86
+ }
87
+ if (hasBound(min) && value < min) {
88
+ throw new Error(`Value must be at least ${min}`);
89
+ }
90
+ if (hasBound(max) && value > max) {
91
+ throw new Error(`Value must be at most ${max}`);
92
+ }
93
+ };
94
+
70
95
  /**
71
96
  * Factory function to create a bounded string scalar
72
97
  * @param {string} name - Name for the scalar
73
- * @param {number} min - Minimum length
74
- * @param {number} max - Maximum length
98
+ * @param {number|null} [min] - Minimum length; null or undefined means no minimum
99
+ * @param {number|null} [max] - Maximum length; null or undefined means no maximum
75
100
  * @returns {GraphQLScalarType} A scalar type with length validation
76
101
  */
77
102
  export const createBoundedStringScalar = (name, min, max) => {
78
103
  return createValidatedScalar(
79
104
  name,
80
- `A string with length between ${min} and ${max} characters`,
105
+ describeBounds(min, max, {
106
+ between: `A string with length between ${min} and ${max} characters`,
107
+ atLeast: `A string with at least ${min} characters`,
108
+ atMost: `A string with at most ${max} characters`,
109
+ unbounded: 'A string',
110
+ }),
81
111
  GraphQLString,
82
112
  (value) => {
83
113
  if (typeof value !== 'string') {
84
114
  throw new Error('Value must be a string');
85
115
  }
86
- if (min !== undefined && value.length < min) {
116
+ if (hasBound(min) && value.length < min) {
87
117
  throw new Error(`String must be at least ${min} characters`);
88
118
  }
89
- if (max !== undefined && value.length > max) {
119
+ if (hasBound(max) && value.length > max) {
90
120
  throw new Error(`String must be at most ${max} characters`);
91
121
  }
92
122
  },
@@ -96,52 +126,42 @@ export const createBoundedStringScalar = (name, min, max) => {
96
126
  /**
97
127
  * Factory function to create a bounded integer scalar
98
128
  * @param {string} name - Name for the scalar
99
- * @param {number} min - Minimum value
100
- * @param {number} max - Maximum value
129
+ * @param {number|null} [min] - Minimum value; null or undefined means no minimum
130
+ * @param {number|null} [max] - Maximum value; null or undefined means no maximum
101
131
  * @returns {GraphQLScalarType} A scalar type with range validation
102
132
  */
103
133
  export const createBoundedIntScalar = (name, min, max) => {
104
134
  return createValidatedScalar(
105
135
  name,
106
- `An integer between ${min} and ${max}`,
136
+ describeBounds(min, max, {
137
+ between: `An integer between ${min} and ${max}`,
138
+ atLeast: `An integer of at least ${min}`,
139
+ atMost: `An integer of at most ${max}`,
140
+ unbounded: 'An integer',
141
+ }),
107
142
  GraphQLInt,
108
- (value) => {
109
- if (typeof value !== 'number' || isNaN(value)) {
110
- throw new Error('Value must be a number');
111
- }
112
- if (min !== undefined && value < min) {
113
- throw new Error(`Value must be at least ${min}`);
114
- }
115
- if (max !== undefined && value > max) {
116
- throw new Error(`Value must be at most ${max}`);
117
- }
118
- },
143
+ checkNumberBounds(min, max),
119
144
  );
120
145
  };
121
146
 
122
147
  /**
123
148
  * Factory function to create a bounded float scalar
124
149
  * @param {string} name - Name for the scalar
125
- * @param {number} min - Minimum value
126
- * @param {number} max - Maximum value
150
+ * @param {number|null} [min] - Minimum value; null or undefined means no minimum
151
+ * @param {number|null} [max] - Maximum value; null or undefined means no maximum
127
152
  * @returns {GraphQLScalarType} A scalar type with range validation
128
153
  */
129
154
  export const createBoundedFloatScalar = (name, min, max) => {
130
155
  return createValidatedScalar(
131
156
  name,
132
- `A float between ${min} and ${max}`,
157
+ describeBounds(min, max, {
158
+ between: `A float between ${min} and ${max}`,
159
+ atLeast: `A float of at least ${min}`,
160
+ atMost: `A float of at most ${max}`,
161
+ unbounded: 'A float',
162
+ }),
133
163
  GraphQLFloat,
134
- (value) => {
135
- if (typeof value !== 'number' || isNaN(value)) {
136
- throw new Error('Value must be a number');
137
- }
138
- if (min !== undefined && value < min) {
139
- throw new Error(`Value must be at least ${min}`);
140
- }
141
- if (max !== undefined && value > max) {
142
- throw new Error(`Value must be at most ${max}`);
143
- }
144
- },
164
+ checkNumberBounds(min, max),
145
165
  );
146
166
  };
147
167
 
package/src/validators.js CHANGED
@@ -43,6 +43,9 @@ const createValidator = (validatorFn, required = false) => {
43
43
  };
44
44
  };
45
45
 
46
+ // A null or omitted bound means no bound (#97). Other values keep JavaScript's comparison, so numeric strings work.
47
+ const hasBound = (bound) => bound !== undefined && bound !== null;
48
+
46
49
  /**
47
50
  * String validators
48
51
  */
@@ -52,11 +55,11 @@ export const stringLength = (name, min, max) => {
52
55
  throw new SimfinityError(`${name} must be a string`, 'VALIDATION_ERROR', 400);
53
56
  }
54
57
 
55
- if (min !== undefined && value.length < min) {
58
+ if (hasBound(min) && value.length < min) {
56
59
  throw new SimfinityError(`${name} must be at least ${min} characters`, 'VALIDATION_ERROR', 400);
57
60
  }
58
61
 
59
- if (max !== undefined && value.length > max) {
62
+ if (hasBound(max) && value.length > max) {
60
63
  throw new SimfinityError(`${name} must be at most ${max} characters`, 'VALIDATION_ERROR', 400);
61
64
  }
62
65
  }, true); // Required for CREATE operations
@@ -68,7 +71,7 @@ export const maxLength = (name, max) => {
68
71
  throw new SimfinityError(`${name} must be a string`, 'VALIDATION_ERROR', 400);
69
72
  }
70
73
 
71
- if (value.length > max) {
74
+ if (hasBound(max) && value.length > max) {
72
75
  throw new SimfinityError(`${name} must be at most ${max} characters`, 'VALIDATION_ERROR', 400);
73
76
  }
74
77
  }, false); // Optional
@@ -125,11 +128,11 @@ export const numberRange = (name, min, max) => {
125
128
  throw new SimfinityError(`${name} must be a number`, 'VALIDATION_ERROR', 400);
126
129
  }
127
130
 
128
- if (min !== undefined && value < min) {
131
+ if (hasBound(min) && value < min) {
129
132
  throw new SimfinityError(`${name} must be at least ${min}`, 'VALIDATION_ERROR', 400);
130
133
  }
131
134
 
132
- if (max !== undefined && value > max) {
135
+ if (hasBound(max) && value > max) {
133
136
  throw new SimfinityError(`${name} must be at most ${max}`, 'VALIDATION_ERROR', 400);
134
137
  }
135
138
  }, false); // Optional
@@ -150,22 +153,96 @@ export const positive = (name) => {
150
153
  /**
151
154
  * Array validators
152
155
  */
156
+
157
+ // What arrayLength() runs on each item (#92). A validator is anything with a validate() function.
158
+ const isFieldValidator = (candidate) => candidate !== null
159
+ && (typeof candidate === 'object' || typeof candidate === 'function')
160
+ && typeof candidate.validate === 'function';
161
+
162
+ // A helper result such as validators.maxLength('Tag', 3). Items of a supplied list are always present, so they get
163
+ // the helper's CREATE rules on create and update, like the documented validators.maxLength('Tag', 3).CREATE.
164
+ const isFieldValidations = (candidate) => candidate !== null && typeof candidate === 'object'
165
+ && !Array.isArray(candidate) && Array.isArray(candidate.CREATE) && candidate.CREATE.every(isFieldValidator);
166
+
167
+ // Array entries: every validator runs, as before, and a helper result runs its CREATE rules.
168
+ const itemRulesOf = (entry) => {
169
+ if (isFieldValidator(entry)) return [entry];
170
+ if (isFieldValidations(entry)) return entry.CREATE;
171
+ return null;
172
+ };
173
+
174
+ // A single itemValidator was ignored before, so only shapes written for Simfinity run now: helper results, and plain
175
+ // objects or functions with validate(). A class instance with its own validate(), such as a Joi or yup schema, takes
176
+ // other arguments, so it stays ignored.
177
+ const isPlainValidator = (candidate) => isFieldValidator(candidate)
178
+ && (typeof candidate === 'function' || [Object.prototype, null].includes(Object.getPrototypeOf(candidate)));
179
+
180
+ const ITEM_VALIDATOR_SHAPES = 'a validator object with validate(), a helper result such as validators.maxLength(\'Item\', 20), or an array of them';
181
+
182
+ // Resolves itemValidator once, when arrayLength() runs. No shape throws here, so every application that starts keeps
183
+ // starting; unusable shapes log one 'Configuration issue' warning instead.
184
+ const itemValidationOf = (name, itemValidator) => {
185
+ // undefined, null, false, '' and 0 add no item checks, so `flag && rules` works.
186
+ if (!itemValidator) return null;
187
+
188
+ if (Array.isArray(itemValidator)) {
189
+ // Falsy entries are skipped. Any other unusable entry keeps failing lists with items, as before.
190
+ const unusable = itemValidator.flatMap((entry, index) => (!entry || itemRulesOf(entry) ? [] : [`itemValidator[${index}]`]));
191
+ if (unusable.length > 0) {
192
+ console.warn(`Configuration issue: validators.arrayLength('${name}') has an unusable ${unusable.join(', ')}; `
193
+ + `lists with items fail with a TypeError. Pass ${ITEM_VALIDATOR_SHAPES}.`);
194
+ }
195
+ // The array is read on every validation, as before, so entries added after creation run too.
196
+ return async (typeName, fieldName, item, session) => {
197
+ for (let index = 0; index < itemValidator.length; index++) {
198
+ const entry = itemValidator[index];
199
+ if (!entry) continue;
200
+ const rules = itemRulesOf(entry);
201
+ if (!rules) {
202
+ throw new TypeError(`validators.arrayLength('${name}'): itemValidator[${index}] is not a validator with a validate() function`);
203
+ }
204
+ for (const rule of rules) {
205
+ await rule.validate(typeName, fieldName, item, session);
206
+ }
207
+ }
208
+ };
209
+ }
210
+
211
+ let rules = null;
212
+ if (isPlainValidator(itemValidator)) {
213
+ rules = [itemValidator];
214
+ } else if (isFieldValidations(itemValidator)) {
215
+ rules = itemValidator.CREATE;
216
+ }
217
+ if (!rules) {
218
+ const shape = isFieldValidator(itemValidator)
219
+ ? 'a class instance whose validate() is not a Simfinity validator (such as a Joi or yup schema)'
220
+ : `not ${ITEM_VALIDATOR_SHAPES}`;
221
+ console.warn(`Configuration issue: validators.arrayLength('${name}') ignores its itemValidator, which is ${shape}; `
222
+ + 'items are not validated.');
223
+ return null;
224
+ }
225
+ return async (typeName, fieldName, item, session) => {
226
+ for (const rule of rules) {
227
+ await rule.validate(typeName, fieldName, item, session);
228
+ }
229
+ };
230
+ };
231
+
153
232
  export const arrayLength = (name, maxItems, itemValidator) => {
233
+ const validateItem = itemValidationOf(name, itemValidator);
154
234
  return createValidator(async (typeName, fieldName, value, session) => {
155
235
  if (!Array.isArray(value)) {
156
236
  throw new SimfinityError(`${name} must be an array`, 'VALIDATION_ERROR', 400);
157
237
  }
158
238
 
159
- if (maxItems !== undefined && value.length > maxItems) {
239
+ if (hasBound(maxItems) && value.length > maxItems) {
160
240
  throw new SimfinityError(`${name} must have at most ${maxItems} items`, 'VALIDATION_ERROR', 400);
161
241
  }
162
242
 
163
- // If itemValidator is provided, validate each item
164
- if (itemValidator && Array.isArray(itemValidator)) {
243
+ if (validateItem) {
165
244
  for (let i = 0; i < value.length; i++) {
166
- for (const validator of itemValidator) {
167
- await validator.validate(typeName, fieldName, value[i], session);
168
- }
245
+ await validateItem(typeName, fieldName, value[i], session);
169
246
  }
170
247
  }
171
248
  }, false); // Optional
@@ -174,52 +251,87 @@ export const arrayLength = (name, maxItems, itemValidator) => {
174
251
  /**
175
252
  * Date validators
176
253
  */
254
+
255
+ // Calendar arithmetic (#94), independent of the process time zone.
256
+ const isLeapYear = (year) => (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
257
+ const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
258
+ const isCalendarDate = (year, month, day) => month >= 1 && month <= 12 && day >= 1
259
+ && day <= (month === 2 && isLeapYear(year) ? 29 : DAYS_IN_MONTH[month - 1]);
260
+
261
+ // The only format dateFormat() checks.
262
+ const ISO_DATE_FORMAT = 'YYYY-MM-DD';
263
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
264
+ // A leading ISO date, alone or followed by a time or a zone.
265
+ const ISO_DATE_PREFIX = /^(\d{4})-(\d{2})-(\d{2})(?!\d)/;
266
+ // JavaScript reads slashed dates month first.
267
+ const SLASHED_DATE = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/;
268
+
269
+ // Strings that JavaScript parses by rolling an impossible day into the next month, such as 2024-02-31 or 02/31/2024.
270
+ // Only values whose stored date never matched what was sent qualify, so no correctly stored value is rejected.
271
+ const isRolledOverDate = (value) => {
272
+ const iso = ISO_DATE_PREFIX.exec(value);
273
+ if (iso) return !isCalendarDate(Number(iso[1]), Number(iso[2]), Number(iso[3]));
274
+ const slashed = SLASHED_DATE.exec(value);
275
+ if (slashed) return !isCalendarDate(Number(slashed[3]), Number(slashed[1]), Number(slashed[2]));
276
+ return false;
277
+ };
278
+
279
+ // Date objects, and strings or timestamps that JavaScript can parse, as before.
280
+ const parseDate = (name, value) => {
281
+ let date;
282
+ if (value instanceof Date) {
283
+ date = value;
284
+ } else if (typeof value === 'string' || typeof value === 'number') {
285
+ date = new Date(value);
286
+ } else {
287
+ throw new SimfinityError(`${name} must be a valid date`, 'VALIDATION_ERROR', 400);
288
+ }
289
+
290
+ if (isNaN(date.getTime())) {
291
+ throw new SimfinityError(`${name} must be a valid date`, 'VALIDATION_ERROR', 400);
292
+ }
293
+ return date;
294
+ };
295
+
296
+ // parseDate, plus the calendar check for strings that JavaScript would roll into the next month.
297
+ const toDate = (name, value) => {
298
+ const date = parseDate(name, value);
299
+ if (typeof value === 'string' && isRolledOverDate(value)) {
300
+ throw new SimfinityError(`${name} must be a valid date`, 'VALIDATION_ERROR', 400);
301
+ }
302
+ return date;
303
+ };
304
+
177
305
  export const dateFormat = (name, format) => {
306
+ const checksFormat = format === ISO_DATE_FORMAT;
307
+ // Other formats were never checked. They keep that behavior, and the warning says so once, here.
308
+ if (format && !checksFormat) {
309
+ const label = typeof format === 'string' ? `'${format}'` : `of type ${typeof format}`;
310
+ console.warn(`Configuration issue: validators.dateFormat('${name}') does not check the format ${label}; `
311
+ + `only '${ISO_DATE_FORMAT}' is checked, so other values pass when JavaScript can parse them as a valid date.`);
312
+ }
313
+
178
314
  return createValidator(async (typeName, fieldName, value) => {
179
- // Handle Date objects, ISO strings, and timestamps
180
- let date;
181
- if (value instanceof Date) {
182
- date = value;
183
- } else if (typeof value === 'string') {
184
- date = new Date(value);
185
- } else if (typeof value === 'number') {
186
- date = new Date(value);
187
- } else {
188
- throw new SimfinityError(`${name} must be a valid date`, 'VALIDATION_ERROR', 400);
189
- }
190
-
191
- if (isNaN(date.getTime())) {
192
- throw new SimfinityError(`${name} must be a valid date`, 'VALIDATION_ERROR', 400);
193
- }
194
-
195
- // If format is provided, validate format
196
- if (format && typeof value === 'string') {
197
- // Simple format validation - can be enhanced
198
- const formatRegex = /^\d{4}-\d{2}-\d{2}$/; // YYYY-MM-DD
199
- if (format === 'YYYY-MM-DD' && !formatRegex.test(value)) {
315
+ if (checksFormat && typeof value === 'string') {
316
+ // As before: an unparseable value is not a valid date, and any other shape gets the format message.
317
+ parseDate(name, value);
318
+ if (!ISO_DATE.test(value)) {
200
319
  throw new SimfinityError(`${name} must be in format ${format}`, 'VALIDATION_ERROR', 400);
201
320
  }
202
- // Add more format patterns as needed
321
+ // A value in the format must be a calendar date.
322
+ if (isRolledOverDate(value)) {
323
+ throw new SimfinityError(`${name} must be a valid date`, 'VALIDATION_ERROR', 400);
324
+ }
325
+ return;
203
326
  }
327
+ // Date objects, timestamps, and strings when no format is checked.
328
+ toDate(name, value);
204
329
  }, false); // Optional
205
330
  };
206
331
 
207
332
  export const futureDate = (name) => {
208
333
  return createValidator(async (typeName, fieldName, value) => {
209
- let date;
210
- if (value instanceof Date) {
211
- date = value;
212
- } else if (typeof value === 'string') {
213
- date = new Date(value);
214
- } else if (typeof value === 'number') {
215
- date = new Date(value);
216
- } else {
217
- throw new SimfinityError(`${name} must be a valid date`, 'VALIDATION_ERROR', 400);
218
- }
219
-
220
- if (isNaN(date.getTime())) {
221
- throw new SimfinityError(`${name} must be a valid date`, 'VALIDATION_ERROR', 400);
222
- }
334
+ const date = toDate(name, value);
223
335
 
224
336
  if (date <= new Date()) {
225
337
  throw new SimfinityError(`${name} must be a future date`, 'VALIDATION_ERROR', 400);
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Declarations for `@simtlix/simfinity-core/auth/errors`. Every member has the type of the
3
+ * same member of the root `auth` namespace, so the two cannot drift.
4
+ */
5
+ import { auth } from '../index.js';
6
+
7
+ /** Authentication error: code `UNAUTHENTICATED`, status 401. */
8
+ export const UnauthenticatedError: typeof auth.UnauthenticatedError;
9
+ export type UnauthenticatedError = InstanceType<typeof auth.UnauthenticatedError>;
10
+ /** Authorization error: code `FORBIDDEN`, status 403. */
11
+ export const ForbiddenError: typeof auth.ForbiddenError;
12
+ export type ForbiddenError = InstanceType<typeof auth.ForbiddenError>;
13
+ export const createAuthError: typeof auth.createAuthError;
14
+
15
+ declare const errors: Pick<typeof auth, 'UnauthenticatedError' | 'ForbiddenError' | 'createAuthError'>;
16
+ export default errors;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Declarations for `@simtlix/simfinity-core/auth/expressions`. Every named export has the type of
3
+ * the same member of the root `auth` namespace, so the two cannot drift.
4
+ */
5
+ import { auth } from '../index.js';
6
+
7
+ export type { PolicyExpression } from '../index.js';
8
+ export const evaluateExpression: typeof auth.evaluateExpression;
9
+ export const isPolicyExpression: typeof auth.isPolicyExpression;
10
+ export const createRuleFromExpression: typeof auth.createRuleFromExpression;
11
+
12
+ declare const expressions: Pick<typeof auth, 'evaluateExpression' | 'isPolicyExpression' | 'createRuleFromExpression'> & {
13
+ /**
14
+ * Low-level helper kept on the default object only: reads a dotted `parent.`, `args.` or `ctx.`
15
+ * path from `{ parent, args, ctx }`; an invalid path gives `undefined`. Prefer
16
+ * `createRuleFromExpression`.
17
+ */
18
+ resolveRef(refPath: string, context: any): unknown;
19
+ /**
20
+ * Low-level helper kept on the default object only: resolves a `{ ref }` operand, or returns a
21
+ * literal as is.
22
+ */
23
+ resolveValue(value: unknown, context: any): unknown;
24
+ };
25
+ export default expressions;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Declarations for `@simtlix/simfinity-core/auth`. The default export is the root `auth`
3
+ * namespace, and every named export has the type of the same member, so the two cannot drift.
4
+ */
5
+ import { auth } from '../index.js';
6
+
7
+ export type {
8
+ AuthPluginOptions,
9
+ AuthRule,
10
+ AuthRuleFunction,
11
+ EnvelopSchemaPlugin,
12
+ PermissionSchema,
13
+ PolicyExpression,
14
+ TypePermissions,
15
+ } from '../index.js';
16
+ export { UnauthenticatedError, ForbiddenError, createAuthError } from './errors.js';
17
+ export { evaluateExpression, isPolicyExpression, createRuleFromExpression } from './expressions.js';
18
+ export {
19
+ resolvePath,
20
+ requireAuth,
21
+ requireRole,
22
+ requirePermission,
23
+ composeRules,
24
+ anyRule,
25
+ isOwner,
26
+ createRule,
27
+ allow,
28
+ deny,
29
+ } from './rules.js';
30
+ export const createAuthPlugin: typeof auth.createAuthPlugin;
31
+ /** @deprecated Use createAuthPlugin instead. graphql-middleware compatible middleware. */
32
+ export const createAuthMiddleware: typeof auth.createAuthMiddleware;
33
+ /** @deprecated Use createAuthPlugin instead. The same function as createAuthMiddleware. */
34
+ export const createFieldMiddleware: typeof auth.createFieldMiddleware;
35
+
36
+ export default auth;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Declarations for `@simtlix/simfinity-core/auth/rules`. Every member has the type of the same
3
+ * member of the root `auth` namespace, so the two cannot drift.
4
+ */
5
+ import { auth } from '../index.js';
6
+
7
+ export type { AuthRuleFunction } from '../index.js';
8
+ export const resolvePath: typeof auth.resolvePath;
9
+ export const requireAuth: typeof auth.requireAuth;
10
+ export const requireRole: typeof auth.requireRole;
11
+ export const requirePermission: typeof auth.requirePermission;
12
+ export const composeRules: typeof auth.composeRules;
13
+ export const anyRule: typeof auth.anyRule;
14
+ export const isOwner: typeof auth.isOwner;
15
+ export const createRule: typeof auth.createRule;
16
+ export const allow: typeof auth.allow;
17
+ export const deny: typeof auth.deny;
18
+
19
+ declare const rules: Pick<typeof auth,
20
+ | 'resolvePath'
21
+ | 'requireAuth'
22
+ | 'requireRole'
23
+ | 'requirePermission'
24
+ | 'composeRules'
25
+ | 'anyRule'
26
+ | 'isOwner'
27
+ | 'createRule'
28
+ | 'allow'
29
+ | 'deny'>;
30
+ export default rules;
package/types/index.d.ts CHANGED
@@ -75,6 +75,13 @@ export class SimfinityError extends Error {
75
75
  getCause?(): unknown;
76
76
  }
77
77
 
78
+ /**
79
+ * An unexpected failure: code `INTERNAL_SERVER_ERROR` and status 500 (`getStatus()` and
80
+ * `extensions.status`). `cause` keeps the original error. The cause is never included in GraphQL
81
+ * responses, or in the errors that `buildErrorFormatter` returns when they are serialized. It is an
82
+ * enumerable own property of this error, so `JSON.stringify(error)`, or a logger that copies
83
+ * enumerable properties, includes it.
84
+ */
78
85
  export class InternalServerError extends SimfinityError {
79
86
  constructor(message: string, cause?: unknown);
80
87
  cause?: unknown;
@@ -106,6 +113,8 @@ export class InternalServerError extends SimfinityError {
106
113
  * - a non-Error value becomes `InternalServerError('Unexpected error value')` with no cause. Servers
107
114
  * whose executor first turns it into an `Error`, such as Yoga, pass the value's text instead.
108
115
  *
116
+ * Every `InternalServerError` has code `INTERNAL_SERVER_ERROR` and status 500.
117
+ *
109
118
  * `callback` receives the classified error; check `error instanceof InternalServerError` to mask
110
119
  * unexpected errors. A returned error replaces it, and a returned `GraphQLError` is returned
111
120
  * unchanged. The result is a `GraphQLError` that keeps the input's locations and path, has the
@@ -326,10 +335,20 @@ export interface FieldValidations {
326
335
  update: FieldValidator[];
327
336
  }
328
337
 
329
- /** Built-in field validator factories. */
338
+ /**
339
+ * What `validators.arrayLength()` runs on each item: a validator, a helper result such as
340
+ * `validators.maxLength('Tag', 20)`, whose `CREATE` rules run on every item on create and update,
341
+ * or an array of them. Falsy array entries are skipped, so `[flag && rule]` works.
342
+ */
343
+ export type ItemValidators =
344
+ | FieldValidator
345
+ | FieldValidations
346
+ | ReadonlyArray<FieldValidator | FieldValidations | null | undefined | false | '' | 0>;
347
+
348
+ /** Built-in field validator factories. A `null` or omitted bound means no bound. */
330
349
  export const validators: {
331
- stringLength(name: string, min?: number, max?: number): FieldValidations;
332
- maxLength(name: string, max: number): FieldValidations;
350
+ stringLength(name: string, min?: number | null, max?: number | null): FieldValidations;
351
+ maxLength(name: string, max: number | null): FieldValidations;
333
352
  /**
334
353
  * Copies the pattern when the helper is created and tests each value from its first character,
335
354
  * so `g` and `y` keep no state between values. Throws `TypeError` unless the pattern is a
@@ -338,10 +357,25 @@ export const validators: {
338
357
  pattern(name: string, regex: RegExp | string, message?: string): FieldValidations;
339
358
  email(): FieldValidations;
340
359
  url(): FieldValidations;
341
- numberRange(name: string, min?: number, max?: number): FieldValidations;
360
+ numberRange(name: string, min?: number | null, max?: number | null): FieldValidations;
342
361
  positive(name: string): FieldValidations;
343
- arrayLength(name: string, maxItems?: number, itemValidator?: FieldValidator[]): FieldValidations;
362
+ /**
363
+ * A falsy `itemValidator` adds no item checks. A single validator runs when it is a plain object or a
364
+ * function; a class instance with its own `validate()`, such as a Joi or yup schema, is ignored with a
365
+ * warning. Other unusable values log a `Configuration issue` warning when the helper is created.
366
+ */
367
+ arrayLength(
368
+ name: string,
369
+ maxItems?: number | null,
370
+ itemValidator?: FieldValidator | FieldValidator[] | FieldValidations | ItemValidators | null | undefined | false | '' | 0,
371
+ ): FieldValidations;
372
+ /**
373
+ * Values must be dates JavaScript can parse. Strings that start with an ISO date (`YYYY-MM-DD`), and
374
+ * slashed `MM/DD/YYYY` strings, must be real calendar dates, so `2024-02-31` and `02/31/2024` fail.
375
+ * Only the `'YYYY-MM-DD'` format is checked; any other format logs a warning when the helper is created.
376
+ */
344
377
  dateFormat(name: string, format?: string): FieldValidations;
378
+ /** Same date checks as `dateFormat()` without a format, plus the date must be in the future. */
345
379
  futureDate(name: string): FieldValidations;
346
380
  };
347
381
 
@@ -355,9 +389,12 @@ export const scalars: {
355
389
  URLScalar: GraphQLScalarType;
356
390
  PositiveIntScalar: GraphQLScalarType;
357
391
  PositiveFloatScalar: GraphQLScalarType;
358
- createBoundedStringScalar(name: string, min?: number, max?: number): GraphQLScalarType;
359
- createBoundedIntScalar(name: string, min?: number, max?: number): GraphQLScalarType;
360
- createBoundedFloatScalar(name: string, min?: number, max?: number): GraphQLScalarType;
392
+ /** A `null` or omitted bound means no bound; the description names only the bounds that are set. */
393
+ createBoundedStringScalar(name: string, min?: number | null, max?: number | null): GraphQLScalarType;
394
+ /** A `null` or omitted bound means no bound; the description names only the bounds that are set. */
395
+ createBoundedIntScalar(name: string, min?: number | null, max?: number | null): GraphQLScalarType;
396
+ /** A `null` or omitted bound means no bound; the description names only the bounds that are set. */
397
+ createBoundedFloatScalar(name: string, min?: number | null, max?: number | null): GraphQLScalarType;
361
398
  /** Same pattern rules as `validators.pattern`: a private copy, tested from the first character. */
362
399
  createPatternStringScalar(name: string, pattern: RegExp | string, message?: string): GraphQLScalarType;
363
400
  };
@@ -429,14 +466,20 @@ export const auth: {
429
466
  * thenable denies with TypeError.
430
467
  */
431
468
  requireAuth(userPath?: string | ((ctx: any) => unknown)): AuthRuleFunction;
432
- /** Required roles must be nonempty strings; invalid configuration throws TypeError. */
469
+ /**
470
+ * Required roles must be nonempty strings; invalid configuration throws TypeError. The array,
471
+ * which may be readonly, is copied when the rule is created.
472
+ */
433
473
  requireRole(
434
- role: string | string[],
474
+ role: string | readonly string[],
435
475
  options?: { userPath?: string | ((ctx: any) => unknown); rolePath?: string | ((user: any) => unknown) },
436
476
  ): AuthRuleFunction;
437
- /** Exact array membership; only a standalone '*' claim grants all permissions. */
477
+ /**
478
+ * Exact array membership; only a standalone '*' claim grants all permissions. The array of
479
+ * required permissions, which may be readonly, is copied when the rule is created.
480
+ */
438
481
  requirePermission(
439
- permission: string | string[],
482
+ permission: string | readonly string[],
440
483
  options?: { userPath?: string | ((ctx: any) => unknown); permissionsPath?: string | ((user: any) => unknown) },
441
484
  ): AuthRuleFunction;
442
485
  /** Requires at least one rule function; throws TypeError otherwise. */
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Declarations for `@simtlix/simfinity-core/plugins`. The default export is the root `plugins`
3
+ * namespace, and every named export has the type of the same member, so the two cannot drift.
4
+ */
5
+ import { plugins } from './index.js';
6
+
7
+ export type { AuthPluginOptions, EnvelopSchemaPlugin, PermissionSchema } from './index.js';
8
+ export const createAuthPlugin: typeof plugins.createAuthPlugin;
9
+ export const apolloCountPlugin: typeof plugins.apolloCountPlugin;
10
+ export const envelopCountPlugin: typeof plugins.envelopCountPlugin;
11
+
12
+ export default plugins;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Declarations for `@simtlix/simfinity-core/scalars`. The default export is the root `scalars`
3
+ * namespace, and every named export has the type of the same member, so the two cannot drift.
4
+ */
5
+ import { scalars } from './index.js';
6
+
7
+ export const EmailScalar: typeof scalars.EmailScalar;
8
+ export const URLScalar: typeof scalars.URLScalar;
9
+ export const PositiveIntScalar: typeof scalars.PositiveIntScalar;
10
+ export const PositiveFloatScalar: typeof scalars.PositiveFloatScalar;
11
+ export const createBoundedStringScalar: typeof scalars.createBoundedStringScalar;
12
+ export const createBoundedIntScalar: typeof scalars.createBoundedIntScalar;
13
+ export const createBoundedFloatScalar: typeof scalars.createBoundedFloatScalar;
14
+ export const createPatternStringScalar: typeof scalars.createPatternStringScalar;
15
+
16
+ export default scalars;