@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/README.md +3 -1
- package/package.json +54 -8
- package/src/const/QLValue.js +11 -3
- package/src/errors/internal-server.error.js +5 -1
- package/src/runtime.js +343 -143
- package/src/scalars.js +53 -33
- package/src/validators.js +159 -47
- package/types/auth/errors.d.ts +16 -0
- package/types/auth/expressions.d.ts +25 -0
- package/types/auth/index.d.ts +36 -0
- package/types/auth/rules.d.ts +30 -0
- package/types/index.d.ts +55 -12
- package/types/plugins.d.ts +12 -0
- package/types/scalars.d.ts +16 -0
- package/types/validators.d.ts +20 -0
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
|
-
|
|
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
|
|
116
|
+
if (hasBound(min) && value.length < min) {
|
|
87
117
|
throw new Error(`String must be at least ${min} characters`);
|
|
88
118
|
}
|
|
89
|
-
if (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
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
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
|
-
(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
164
|
-
if (itemValidator && Array.isArray(itemValidator)) {
|
|
243
|
+
if (validateItem) {
|
|
165
244
|
for (let i = 0; i < value.length; i++) {
|
|
166
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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;
|