@zero-server/sdk 0.9.1 → 0.9.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/LICENSE +21 -21
- package/README.md +460 -443
- package/index.js +414 -412
- package/lib/app.js +1172 -1172
- package/lib/auth/authorize.js +399 -399
- package/lib/auth/enrollment.js +367 -367
- package/lib/auth/index.js +57 -57
- package/lib/auth/jwt.js +731 -731
- package/lib/auth/oauth.js +362 -362
- package/lib/auth/session.js +588 -588
- package/lib/auth/trustedDevice.js +409 -409
- package/lib/auth/twoFactor.js +1150 -1150
- package/lib/auth/webauthn.js +946 -946
- package/lib/body/index.js +14 -14
- package/lib/body/json.js +109 -109
- package/lib/body/multipart.js +440 -440
- package/lib/body/raw.js +71 -71
- package/lib/body/rawBuffer.js +160 -160
- package/lib/body/sendError.js +25 -25
- package/lib/body/text.js +75 -75
- package/lib/body/typeMatch.js +41 -41
- package/lib/body/urlencoded.js +235 -235
- package/lib/cli.js +845 -845
- package/lib/cluster.js +666 -666
- package/lib/debug.js +372 -372
- package/lib/env/index.js +465 -465
- package/lib/errors.js +683 -683
- package/lib/fetch/index.js +256 -256
- package/lib/grpc/balancer.js +378 -378
- package/lib/grpc/call.js +708 -708
- package/lib/grpc/client.js +764 -764
- package/lib/grpc/codec.js +1221 -1221
- package/lib/grpc/credentials.js +398 -398
- package/lib/grpc/frame.js +262 -262
- package/lib/grpc/health.js +287 -287
- package/lib/grpc/index.js +121 -121
- package/lib/grpc/metadata.js +461 -461
- package/lib/grpc/proto.js +821 -821
- package/lib/grpc/reflection.js +590 -590
- package/lib/grpc/server.js +445 -445
- package/lib/grpc/status.js +118 -118
- package/lib/grpc/watch.js +173 -173
- package/lib/http/index.js +10 -10
- package/lib/http/request.js +727 -727
- package/lib/http/response.js +799 -799
- package/lib/lifecycle.js +557 -557
- package/lib/middleware/compress.js +230 -230
- package/lib/middleware/cookieParser.js +237 -237
- package/lib/middleware/cors.js +93 -93
- package/lib/middleware/csrf.js +137 -137
- package/lib/middleware/errorHandler.js +101 -101
- package/lib/middleware/helmet.js +175 -175
- package/lib/middleware/index.js +19 -17
- package/lib/middleware/logger.js +74 -74
- package/lib/middleware/rateLimit.js +88 -88
- package/lib/middleware/requestId.js +53 -53
- package/lib/middleware/static.js +326 -326
- package/lib/middleware/timeout.js +71 -71
- package/lib/middleware/validator.js +255 -255
- package/lib/observe/health.js +326 -326
- package/lib/observe/index.js +50 -50
- package/lib/observe/logger.js +359 -359
- package/lib/observe/metrics.js +805 -805
- package/lib/observe/tracing.js +592 -592
- package/lib/orm/adapters/json.js +290 -290
- package/lib/orm/adapters/memory.js +764 -764
- package/lib/orm/adapters/mongo.js +764 -764
- package/lib/orm/adapters/mysql.js +933 -933
- package/lib/orm/adapters/postgres.js +1144 -1144
- package/lib/orm/adapters/redis.js +1534 -1534
- package/lib/orm/adapters/sql-base.js +212 -212
- package/lib/orm/adapters/sqlite.js +858 -858
- package/lib/orm/audit.js +649 -649
- package/lib/orm/cache.js +394 -394
- package/lib/orm/geo.js +387 -387
- package/lib/orm/index.js +784 -784
- package/lib/orm/migrate.js +432 -432
- package/lib/orm/model.js +1706 -1706
- package/lib/orm/plugin.js +375 -375
- package/lib/orm/procedures.js +836 -836
- package/lib/orm/profiler.js +233 -233
- package/lib/orm/query.js +1772 -1772
- package/lib/orm/replicas.js +241 -241
- package/lib/orm/schema.js +307 -307
- package/lib/orm/search.js +380 -380
- package/lib/orm/seed/data/commerce.js +136 -136
- package/lib/orm/seed/data/internet.js +111 -111
- package/lib/orm/seed/data/locations.js +204 -204
- package/lib/orm/seed/data/names.js +338 -338
- package/lib/orm/seed/data/person.js +128 -128
- package/lib/orm/seed/data/phone.js +211 -211
- package/lib/orm/seed/data/words.js +134 -134
- package/lib/orm/seed/factory.js +178 -178
- package/lib/orm/seed/fake.js +1186 -1186
- package/lib/orm/seed/index.js +18 -18
- package/lib/orm/seed/rng.js +70 -70
- package/lib/orm/seed/seeder.js +124 -124
- package/lib/orm/seed/unique.js +68 -68
- package/lib/orm/snapshot.js +366 -366
- package/lib/orm/tenancy.js +605 -605
- package/lib/orm/views.js +350 -350
- package/lib/router/index.js +436 -436
- package/lib/sse/index.js +8 -8
- package/lib/sse/stream.js +349 -349
- package/lib/ws/connection.js +451 -451
- package/lib/ws/handshake.js +125 -125
- package/lib/ws/index.js +14 -14
- package/lib/ws/room.js +223 -223
- package/package.json +73 -73
- package/types/app.d.ts +223 -223
- package/types/auth.d.ts +520 -520
- package/types/body.d.ts +14 -0
- package/types/cli.d.ts +2 -0
- package/types/cluster.d.ts +75 -75
- package/types/env.d.ts +80 -80
- package/types/errors.d.ts +316 -316
- package/types/fetch.d.ts +43 -43
- package/types/grpc.d.ts +432 -432
- package/types/index.d.ts +384 -384
- package/types/lifecycle.d.ts +60 -60
- package/types/middleware.d.ts +320 -320
- package/types/observe.d.ts +304 -304
- package/types/orm.d.ts +1887 -1887
- package/types/request.d.ts +109 -109
- package/types/response.d.ts +157 -157
- package/types/router.d.ts +78 -78
- package/types/sse.d.ts +78 -78
- package/types/websocket.d.ts +126 -126
package/lib/orm/schema.js
CHANGED
|
@@ -1,307 +1,307 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module orm/schema
|
|
3
|
-
* @description Schema definition and validation for ORM models.
|
|
4
|
-
* Validates data against column definitions, coerces types,
|
|
5
|
-
* and enforces constraints (required, unique, min, max, enum, match).
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* const { TYPES, validate } = require('@zero-server/sdk').Schema;
|
|
9
|
-
*
|
|
10
|
-
* const columns = {
|
|
11
|
-
* name: { type: TYPES.STRING, required: true, minLength: 1 },
|
|
12
|
-
* email: { type: TYPES.STRING, required: true, match: /@/ },
|
|
13
|
-
* age: { type: TYPES.INTEGER, min: 0, max: 150 },
|
|
14
|
-
* };
|
|
15
|
-
*
|
|
16
|
-
* const { valid, errors, sanitized } = validate(
|
|
17
|
-
* { name: 'Alice', email: 'alice@example.com', age: '30' },
|
|
18
|
-
* columns,
|
|
19
|
-
* );
|
|
20
|
-
* // valid: true, sanitized.age === 30 (coerced from string)
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Supported column types.
|
|
25
|
-
* @enum {string}
|
|
26
|
-
*/
|
|
27
|
-
const TYPES = {
|
|
28
|
-
STRING: 'string',
|
|
29
|
-
INTEGER: 'integer',
|
|
30
|
-
FLOAT: 'float',
|
|
31
|
-
BOOLEAN: 'boolean',
|
|
32
|
-
DATE: 'date',
|
|
33
|
-
DATETIME: 'datetime',
|
|
34
|
-
JSON: 'json',
|
|
35
|
-
TEXT: 'text',
|
|
36
|
-
BLOB: 'blob',
|
|
37
|
-
UUID: 'uuid',
|
|
38
|
-
// Extended numeric types
|
|
39
|
-
BIGINT: 'bigint',
|
|
40
|
-
SMALLINT: 'smallint',
|
|
41
|
-
TINYINT: 'tinyint',
|
|
42
|
-
DECIMAL: 'decimal',
|
|
43
|
-
DOUBLE: 'double',
|
|
44
|
-
REAL: 'real',
|
|
45
|
-
// Extended string / binary types
|
|
46
|
-
CHAR: 'char',
|
|
47
|
-
BINARY: 'binary',
|
|
48
|
-
VARBINARY:'varbinary',
|
|
49
|
-
// Temporal types
|
|
50
|
-
TIMESTAMP:'timestamp',
|
|
51
|
-
TIME: 'time',
|
|
52
|
-
// MySQL-specific
|
|
53
|
-
ENUM: 'enum',
|
|
54
|
-
SET: 'set',
|
|
55
|
-
MEDIUMTEXT: 'mediumtext',
|
|
56
|
-
LONGTEXT: 'longtext',
|
|
57
|
-
MEDIUMBLOB: 'mediumblob',
|
|
58
|
-
LONGBLOB: 'longblob',
|
|
59
|
-
YEAR: 'year',
|
|
60
|
-
// PostgreSQL-specific
|
|
61
|
-
SERIAL: 'serial',
|
|
62
|
-
BIGSERIAL:'bigserial',
|
|
63
|
-
JSONB: 'jsonb',
|
|
64
|
-
INTERVAL: 'interval',
|
|
65
|
-
INET: 'inet',
|
|
66
|
-
CIDR: 'cidr',
|
|
67
|
-
MACADDR: 'macaddr',
|
|
68
|
-
MONEY: 'money',
|
|
69
|
-
XML: 'xml',
|
|
70
|
-
CITEXT: 'citext',
|
|
71
|
-
ARRAY: 'array',
|
|
72
|
-
// SQLite
|
|
73
|
-
NUMERIC: 'numeric',
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Validate and sanitise a single value against a column definition.
|
|
78
|
-
*
|
|
79
|
-
* @param {*} value - Raw input value.
|
|
80
|
-
* @param {object} colDef - Column definition.
|
|
81
|
-
* @param {string} colName - Column name (for error messages).
|
|
82
|
-
* @returns {*} Coerced value.
|
|
83
|
-
* @throws {Error} On validation failure.
|
|
84
|
-
*/
|
|
85
|
-
function validateValue(value, colDef, colName)
|
|
86
|
-
{
|
|
87
|
-
const type = colDef.type || 'string';
|
|
88
|
-
|
|
89
|
-
// Handle null/undefined
|
|
90
|
-
if (value === undefined || value === null)
|
|
91
|
-
{
|
|
92
|
-
if (colDef.required && colDef.default === undefined)
|
|
93
|
-
throw new Error(`"${colName}" is required`);
|
|
94
|
-
if (colDef.default !== undefined)
|
|
95
|
-
return typeof colDef.default === 'function' ? colDef.default() : colDef.default;
|
|
96
|
-
return colDef.nullable !== false ? null : undefined;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
switch (type)
|
|
100
|
-
{
|
|
101
|
-
case 'string':
|
|
102
|
-
case 'text':
|
|
103
|
-
case 'mediumtext':
|
|
104
|
-
case 'longtext':
|
|
105
|
-
case 'char':
|
|
106
|
-
case 'citext':
|
|
107
|
-
case 'xml':
|
|
108
|
-
{
|
|
109
|
-
const val = String(value);
|
|
110
|
-
if (colDef.minLength !== undefined && val.length < colDef.minLength)
|
|
111
|
-
throw new Error(`"${colName}" must be at least ${colDef.minLength} characters`);
|
|
112
|
-
if (colDef.maxLength !== undefined && val.length > colDef.maxLength)
|
|
113
|
-
throw new Error(`"${colName}" must be at most ${colDef.maxLength} characters`);
|
|
114
|
-
if (colDef.match && !colDef.match.test(val))
|
|
115
|
-
throw new Error(`"${colName}" does not match pattern ${colDef.match}`);
|
|
116
|
-
if (colDef.enum && !colDef.enum.includes(val))
|
|
117
|
-
throw new Error(`"${colName}" must be one of [${colDef.enum.join(', ')}]`);
|
|
118
|
-
// Sanitise: prevent SQL-like injection patterns in string values
|
|
119
|
-
return val;
|
|
120
|
-
}
|
|
121
|
-
case 'integer':
|
|
122
|
-
case 'bigint':
|
|
123
|
-
case 'smallint':
|
|
124
|
-
case 'tinyint':
|
|
125
|
-
case 'serial':
|
|
126
|
-
case 'bigserial':
|
|
127
|
-
case 'year':
|
|
128
|
-
{
|
|
129
|
-
const val = typeof value === 'string' ? parseInt(value, 10) : Math.floor(Number(value));
|
|
130
|
-
if (isNaN(val)) throw new Error(`"${colName}" must be an integer`);
|
|
131
|
-
if (colDef.min !== undefined && val < colDef.min)
|
|
132
|
-
throw new Error(`"${colName}" must be >= ${colDef.min}`);
|
|
133
|
-
if (colDef.max !== undefined && val > colDef.max)
|
|
134
|
-
throw new Error(`"${colName}" must be <= ${colDef.max}`);
|
|
135
|
-
return val;
|
|
136
|
-
}
|
|
137
|
-
case 'float':
|
|
138
|
-
case 'decimal':
|
|
139
|
-
case 'double':
|
|
140
|
-
case 'real':
|
|
141
|
-
case 'numeric':
|
|
142
|
-
case 'money':
|
|
143
|
-
{
|
|
144
|
-
const val = Number(value);
|
|
145
|
-
if (isNaN(val)) throw new Error(`"${colName}" must be a number`);
|
|
146
|
-
if (colDef.min !== undefined && val < colDef.min)
|
|
147
|
-
throw new Error(`"${colName}" must be >= ${colDef.min}`);
|
|
148
|
-
if (colDef.max !== undefined && val > colDef.max)
|
|
149
|
-
throw new Error(`"${colName}" must be <= ${colDef.max}`);
|
|
150
|
-
return val;
|
|
151
|
-
}
|
|
152
|
-
case 'boolean':
|
|
153
|
-
{
|
|
154
|
-
if (typeof value === 'boolean') return value;
|
|
155
|
-
if (typeof value === 'string')
|
|
156
|
-
{
|
|
157
|
-
const lower = value.toLowerCase();
|
|
158
|
-
if (['true', '1', 'yes'].includes(lower)) return true;
|
|
159
|
-
if (['false', '0', 'no'].includes(lower)) return false;
|
|
160
|
-
}
|
|
161
|
-
if (typeof value === 'number') return value !== 0;
|
|
162
|
-
throw new Error(`"${colName}" must be a boolean`);
|
|
163
|
-
}
|
|
164
|
-
case 'date':
|
|
165
|
-
case 'datetime':
|
|
166
|
-
case 'timestamp':
|
|
167
|
-
case 'time':
|
|
168
|
-
case 'interval':
|
|
169
|
-
{
|
|
170
|
-
if (value instanceof Date) return value;
|
|
171
|
-
const d = new Date(value);
|
|
172
|
-
if (isNaN(d.getTime())) throw new Error(`"${colName}" must be a valid date`);
|
|
173
|
-
return d;
|
|
174
|
-
}
|
|
175
|
-
case 'json':
|
|
176
|
-
case 'jsonb':
|
|
177
|
-
{
|
|
178
|
-
if (typeof value === 'string')
|
|
179
|
-
{
|
|
180
|
-
try { return JSON.parse(value); }
|
|
181
|
-
catch (e) { throw new Error(`"${colName}" must be valid JSON`); }
|
|
182
|
-
}
|
|
183
|
-
// Already an object/array — return as-is for storage
|
|
184
|
-
return value;
|
|
185
|
-
}
|
|
186
|
-
case 'uuid':
|
|
187
|
-
{
|
|
188
|
-
const val = String(value);
|
|
189
|
-
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(val))
|
|
190
|
-
throw new Error(`"${colName}" must be a valid UUID`);
|
|
191
|
-
return val;
|
|
192
|
-
}
|
|
193
|
-
case 'blob':
|
|
194
|
-
case 'mediumblob':
|
|
195
|
-
case 'longblob':
|
|
196
|
-
case 'binary':
|
|
197
|
-
case 'varbinary':
|
|
198
|
-
return Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
199
|
-
case 'enum':
|
|
200
|
-
{
|
|
201
|
-
const val = String(value);
|
|
202
|
-
if (colDef.enum && !colDef.enum.includes(val))
|
|
203
|
-
throw new Error(`"${colName}" must be one of [${colDef.enum.join(', ')}]`);
|
|
204
|
-
return val;
|
|
205
|
-
}
|
|
206
|
-
case 'set':
|
|
207
|
-
{
|
|
208
|
-
const vals = Array.isArray(value) ? value : String(value).split(',');
|
|
209
|
-
if (colDef.values)
|
|
210
|
-
{
|
|
211
|
-
for (const v of vals)
|
|
212
|
-
if (!colDef.values.includes(v.trim()))
|
|
213
|
-
throw new Error(`"${colName}" contains invalid value "${v.trim()}". Allowed: [${colDef.values.join(', ')}]`);
|
|
214
|
-
}
|
|
215
|
-
return vals.map(v => v.trim()).join(',');
|
|
216
|
-
}
|
|
217
|
-
case 'inet':
|
|
218
|
-
case 'cidr':
|
|
219
|
-
case 'macaddr':
|
|
220
|
-
return String(value);
|
|
221
|
-
case 'array':
|
|
222
|
-
return Array.isArray(value) ? value : [value];
|
|
223
|
-
default:
|
|
224
|
-
return value;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// -- Validation -------------------------------------------
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Validate all columns of a data object against the schema.
|
|
232
|
-
*
|
|
233
|
-
* @param {object} data - Input data object.
|
|
234
|
-
* @param {object} columns - Schema column definitions.
|
|
235
|
-
* @param {object} [options] - Configuration options.
|
|
236
|
-
* @param {boolean} [options.partial=false] - When true, only validates provided fields (for updates).
|
|
237
|
-
* @returns {{ valid: boolean, errors: string[], sanitized: object }}
|
|
238
|
-
*/
|
|
239
|
-
function validate(data, columns, options = {})
|
|
240
|
-
{
|
|
241
|
-
const errors = [];
|
|
242
|
-
const sanitized = {};
|
|
243
|
-
|
|
244
|
-
for (const [colName, colDef] of Object.entries(columns))
|
|
245
|
-
{
|
|
246
|
-
// Skip auto fields on create
|
|
247
|
-
if (colDef.primaryKey && colDef.autoIncrement && data[colName] === undefined) continue;
|
|
248
|
-
|
|
249
|
-
if (options.partial && data[colName] === undefined) continue;
|
|
250
|
-
|
|
251
|
-
// Skip guarded fields not present in data (stripped by mass-assignment)
|
|
252
|
-
if (colDef.guarded && data[colName] === undefined) continue;
|
|
253
|
-
|
|
254
|
-
try
|
|
255
|
-
{
|
|
256
|
-
sanitized[colName] = validateValue(data[colName], colDef, colName);
|
|
257
|
-
}
|
|
258
|
-
catch (e)
|
|
259
|
-
{
|
|
260
|
-
errors.push(e.message);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Reject unknown keys (prevent mass-assignment)
|
|
265
|
-
for (const key of Object.keys(data))
|
|
266
|
-
{
|
|
267
|
-
if (!columns[key])
|
|
268
|
-
{
|
|
269
|
-
errors.push(`Unknown column "${key}"`);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return { valid: errors.length === 0, errors, sanitized };
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// -- DDL Security Helpers --------------------------------
|
|
277
|
-
|
|
278
|
-
const VALID_FK_ACTIONS = new Set(['CASCADE', 'SET NULL', 'SET DEFAULT', 'RESTRICT', 'NO ACTION']);
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Validate and return a FK action string, or throw.
|
|
282
|
-
* @param {string} action - Foreign key action string (CASCADE, SET NULL, etc.).
|
|
283
|
-
* @returns {string} Uppercase validated action
|
|
284
|
-
*/
|
|
285
|
-
function validateFKAction(action)
|
|
286
|
-
{
|
|
287
|
-
const upper = String(action).toUpperCase();
|
|
288
|
-
if (!VALID_FK_ACTIONS.has(upper))
|
|
289
|
-
throw new Error(`Invalid FK action: "${action}". Allowed: ${[...VALID_FK_ACTIONS].join(', ')}`);
|
|
290
|
-
return upper;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Validate a CHECK expression for dangerous SQL patterns.
|
|
295
|
-
* @param {string} expr - SQL CHECK expression to validate.
|
|
296
|
-
* @returns {string} The original expression (validated).
|
|
297
|
-
*/
|
|
298
|
-
function validateCheck(expr)
|
|
299
|
-
{
|
|
300
|
-
const s = String(expr);
|
|
301
|
-
// Block semicolons, comment markers, and common injection patterns
|
|
302
|
-
if (/;|--|\bDROP\b|\bDELETE\b|\bINSERT\b|\bUPDATE\b|\bALTER\b|\bCREATE\b|\bEXEC\b/i.test(s))
|
|
303
|
-
throw new Error(`Potentially dangerous CHECK expression: "${s}"`);
|
|
304
|
-
return s;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
module.exports = { TYPES, validateValue, validate, validateFKAction, validateCheck };
|
|
1
|
+
/**
|
|
2
|
+
* @module orm/schema
|
|
3
|
+
* @description Schema definition and validation for ORM models.
|
|
4
|
+
* Validates data against column definitions, coerces types,
|
|
5
|
+
* and enforces constraints (required, unique, min, max, enum, match).
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const { TYPES, validate } = require('@zero-server/sdk').Schema;
|
|
9
|
+
*
|
|
10
|
+
* const columns = {
|
|
11
|
+
* name: { type: TYPES.STRING, required: true, minLength: 1 },
|
|
12
|
+
* email: { type: TYPES.STRING, required: true, match: /@/ },
|
|
13
|
+
* age: { type: TYPES.INTEGER, min: 0, max: 150 },
|
|
14
|
+
* };
|
|
15
|
+
*
|
|
16
|
+
* const { valid, errors, sanitized } = validate(
|
|
17
|
+
* { name: 'Alice', email: 'alice@example.com', age: '30' },
|
|
18
|
+
* columns,
|
|
19
|
+
* );
|
|
20
|
+
* // valid: true, sanitized.age === 30 (coerced from string)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Supported column types.
|
|
25
|
+
* @enum {string}
|
|
26
|
+
*/
|
|
27
|
+
const TYPES = {
|
|
28
|
+
STRING: 'string',
|
|
29
|
+
INTEGER: 'integer',
|
|
30
|
+
FLOAT: 'float',
|
|
31
|
+
BOOLEAN: 'boolean',
|
|
32
|
+
DATE: 'date',
|
|
33
|
+
DATETIME: 'datetime',
|
|
34
|
+
JSON: 'json',
|
|
35
|
+
TEXT: 'text',
|
|
36
|
+
BLOB: 'blob',
|
|
37
|
+
UUID: 'uuid',
|
|
38
|
+
// Extended numeric types
|
|
39
|
+
BIGINT: 'bigint',
|
|
40
|
+
SMALLINT: 'smallint',
|
|
41
|
+
TINYINT: 'tinyint',
|
|
42
|
+
DECIMAL: 'decimal',
|
|
43
|
+
DOUBLE: 'double',
|
|
44
|
+
REAL: 'real',
|
|
45
|
+
// Extended string / binary types
|
|
46
|
+
CHAR: 'char',
|
|
47
|
+
BINARY: 'binary',
|
|
48
|
+
VARBINARY:'varbinary',
|
|
49
|
+
// Temporal types
|
|
50
|
+
TIMESTAMP:'timestamp',
|
|
51
|
+
TIME: 'time',
|
|
52
|
+
// MySQL-specific
|
|
53
|
+
ENUM: 'enum',
|
|
54
|
+
SET: 'set',
|
|
55
|
+
MEDIUMTEXT: 'mediumtext',
|
|
56
|
+
LONGTEXT: 'longtext',
|
|
57
|
+
MEDIUMBLOB: 'mediumblob',
|
|
58
|
+
LONGBLOB: 'longblob',
|
|
59
|
+
YEAR: 'year',
|
|
60
|
+
// PostgreSQL-specific
|
|
61
|
+
SERIAL: 'serial',
|
|
62
|
+
BIGSERIAL:'bigserial',
|
|
63
|
+
JSONB: 'jsonb',
|
|
64
|
+
INTERVAL: 'interval',
|
|
65
|
+
INET: 'inet',
|
|
66
|
+
CIDR: 'cidr',
|
|
67
|
+
MACADDR: 'macaddr',
|
|
68
|
+
MONEY: 'money',
|
|
69
|
+
XML: 'xml',
|
|
70
|
+
CITEXT: 'citext',
|
|
71
|
+
ARRAY: 'array',
|
|
72
|
+
// SQLite
|
|
73
|
+
NUMERIC: 'numeric',
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Validate and sanitise a single value against a column definition.
|
|
78
|
+
*
|
|
79
|
+
* @param {*} value - Raw input value.
|
|
80
|
+
* @param {object} colDef - Column definition.
|
|
81
|
+
* @param {string} colName - Column name (for error messages).
|
|
82
|
+
* @returns {*} Coerced value.
|
|
83
|
+
* @throws {Error} On validation failure.
|
|
84
|
+
*/
|
|
85
|
+
function validateValue(value, colDef, colName)
|
|
86
|
+
{
|
|
87
|
+
const type = colDef.type || 'string';
|
|
88
|
+
|
|
89
|
+
// Handle null/undefined
|
|
90
|
+
if (value === undefined || value === null)
|
|
91
|
+
{
|
|
92
|
+
if (colDef.required && colDef.default === undefined)
|
|
93
|
+
throw new Error(`"${colName}" is required`);
|
|
94
|
+
if (colDef.default !== undefined)
|
|
95
|
+
return typeof colDef.default === 'function' ? colDef.default() : colDef.default;
|
|
96
|
+
return colDef.nullable !== false ? null : undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
switch (type)
|
|
100
|
+
{
|
|
101
|
+
case 'string':
|
|
102
|
+
case 'text':
|
|
103
|
+
case 'mediumtext':
|
|
104
|
+
case 'longtext':
|
|
105
|
+
case 'char':
|
|
106
|
+
case 'citext':
|
|
107
|
+
case 'xml':
|
|
108
|
+
{
|
|
109
|
+
const val = String(value);
|
|
110
|
+
if (colDef.minLength !== undefined && val.length < colDef.minLength)
|
|
111
|
+
throw new Error(`"${colName}" must be at least ${colDef.minLength} characters`);
|
|
112
|
+
if (colDef.maxLength !== undefined && val.length > colDef.maxLength)
|
|
113
|
+
throw new Error(`"${colName}" must be at most ${colDef.maxLength} characters`);
|
|
114
|
+
if (colDef.match && !colDef.match.test(val))
|
|
115
|
+
throw new Error(`"${colName}" does not match pattern ${colDef.match}`);
|
|
116
|
+
if (colDef.enum && !colDef.enum.includes(val))
|
|
117
|
+
throw new Error(`"${colName}" must be one of [${colDef.enum.join(', ')}]`);
|
|
118
|
+
// Sanitise: prevent SQL-like injection patterns in string values
|
|
119
|
+
return val;
|
|
120
|
+
}
|
|
121
|
+
case 'integer':
|
|
122
|
+
case 'bigint':
|
|
123
|
+
case 'smallint':
|
|
124
|
+
case 'tinyint':
|
|
125
|
+
case 'serial':
|
|
126
|
+
case 'bigserial':
|
|
127
|
+
case 'year':
|
|
128
|
+
{
|
|
129
|
+
const val = typeof value === 'string' ? parseInt(value, 10) : Math.floor(Number(value));
|
|
130
|
+
if (isNaN(val)) throw new Error(`"${colName}" must be an integer`);
|
|
131
|
+
if (colDef.min !== undefined && val < colDef.min)
|
|
132
|
+
throw new Error(`"${colName}" must be >= ${colDef.min}`);
|
|
133
|
+
if (colDef.max !== undefined && val > colDef.max)
|
|
134
|
+
throw new Error(`"${colName}" must be <= ${colDef.max}`);
|
|
135
|
+
return val;
|
|
136
|
+
}
|
|
137
|
+
case 'float':
|
|
138
|
+
case 'decimal':
|
|
139
|
+
case 'double':
|
|
140
|
+
case 'real':
|
|
141
|
+
case 'numeric':
|
|
142
|
+
case 'money':
|
|
143
|
+
{
|
|
144
|
+
const val = Number(value);
|
|
145
|
+
if (isNaN(val)) throw new Error(`"${colName}" must be a number`);
|
|
146
|
+
if (colDef.min !== undefined && val < colDef.min)
|
|
147
|
+
throw new Error(`"${colName}" must be >= ${colDef.min}`);
|
|
148
|
+
if (colDef.max !== undefined && val > colDef.max)
|
|
149
|
+
throw new Error(`"${colName}" must be <= ${colDef.max}`);
|
|
150
|
+
return val;
|
|
151
|
+
}
|
|
152
|
+
case 'boolean':
|
|
153
|
+
{
|
|
154
|
+
if (typeof value === 'boolean') return value;
|
|
155
|
+
if (typeof value === 'string')
|
|
156
|
+
{
|
|
157
|
+
const lower = value.toLowerCase();
|
|
158
|
+
if (['true', '1', 'yes'].includes(lower)) return true;
|
|
159
|
+
if (['false', '0', 'no'].includes(lower)) return false;
|
|
160
|
+
}
|
|
161
|
+
if (typeof value === 'number') return value !== 0;
|
|
162
|
+
throw new Error(`"${colName}" must be a boolean`);
|
|
163
|
+
}
|
|
164
|
+
case 'date':
|
|
165
|
+
case 'datetime':
|
|
166
|
+
case 'timestamp':
|
|
167
|
+
case 'time':
|
|
168
|
+
case 'interval':
|
|
169
|
+
{
|
|
170
|
+
if (value instanceof Date) return value;
|
|
171
|
+
const d = new Date(value);
|
|
172
|
+
if (isNaN(d.getTime())) throw new Error(`"${colName}" must be a valid date`);
|
|
173
|
+
return d;
|
|
174
|
+
}
|
|
175
|
+
case 'json':
|
|
176
|
+
case 'jsonb':
|
|
177
|
+
{
|
|
178
|
+
if (typeof value === 'string')
|
|
179
|
+
{
|
|
180
|
+
try { return JSON.parse(value); }
|
|
181
|
+
catch (e) { throw new Error(`"${colName}" must be valid JSON`); }
|
|
182
|
+
}
|
|
183
|
+
// Already an object/array — return as-is for storage
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
case 'uuid':
|
|
187
|
+
{
|
|
188
|
+
const val = String(value);
|
|
189
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(val))
|
|
190
|
+
throw new Error(`"${colName}" must be a valid UUID`);
|
|
191
|
+
return val;
|
|
192
|
+
}
|
|
193
|
+
case 'blob':
|
|
194
|
+
case 'mediumblob':
|
|
195
|
+
case 'longblob':
|
|
196
|
+
case 'binary':
|
|
197
|
+
case 'varbinary':
|
|
198
|
+
return Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
199
|
+
case 'enum':
|
|
200
|
+
{
|
|
201
|
+
const val = String(value);
|
|
202
|
+
if (colDef.enum && !colDef.enum.includes(val))
|
|
203
|
+
throw new Error(`"${colName}" must be one of [${colDef.enum.join(', ')}]`);
|
|
204
|
+
return val;
|
|
205
|
+
}
|
|
206
|
+
case 'set':
|
|
207
|
+
{
|
|
208
|
+
const vals = Array.isArray(value) ? value : String(value).split(',');
|
|
209
|
+
if (colDef.values)
|
|
210
|
+
{
|
|
211
|
+
for (const v of vals)
|
|
212
|
+
if (!colDef.values.includes(v.trim()))
|
|
213
|
+
throw new Error(`"${colName}" contains invalid value "${v.trim()}". Allowed: [${colDef.values.join(', ')}]`);
|
|
214
|
+
}
|
|
215
|
+
return vals.map(v => v.trim()).join(',');
|
|
216
|
+
}
|
|
217
|
+
case 'inet':
|
|
218
|
+
case 'cidr':
|
|
219
|
+
case 'macaddr':
|
|
220
|
+
return String(value);
|
|
221
|
+
case 'array':
|
|
222
|
+
return Array.isArray(value) ? value : [value];
|
|
223
|
+
default:
|
|
224
|
+
return value;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// -- Validation -------------------------------------------
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Validate all columns of a data object against the schema.
|
|
232
|
+
*
|
|
233
|
+
* @param {object} data - Input data object.
|
|
234
|
+
* @param {object} columns - Schema column definitions.
|
|
235
|
+
* @param {object} [options] - Configuration options.
|
|
236
|
+
* @param {boolean} [options.partial=false] - When true, only validates provided fields (for updates).
|
|
237
|
+
* @returns {{ valid: boolean, errors: string[], sanitized: object }}
|
|
238
|
+
*/
|
|
239
|
+
function validate(data, columns, options = {})
|
|
240
|
+
{
|
|
241
|
+
const errors = [];
|
|
242
|
+
const sanitized = {};
|
|
243
|
+
|
|
244
|
+
for (const [colName, colDef] of Object.entries(columns))
|
|
245
|
+
{
|
|
246
|
+
// Skip auto fields on create
|
|
247
|
+
if (colDef.primaryKey && colDef.autoIncrement && data[colName] === undefined) continue;
|
|
248
|
+
|
|
249
|
+
if (options.partial && data[colName] === undefined) continue;
|
|
250
|
+
|
|
251
|
+
// Skip guarded fields not present in data (stripped by mass-assignment)
|
|
252
|
+
if (colDef.guarded && data[colName] === undefined) continue;
|
|
253
|
+
|
|
254
|
+
try
|
|
255
|
+
{
|
|
256
|
+
sanitized[colName] = validateValue(data[colName], colDef, colName);
|
|
257
|
+
}
|
|
258
|
+
catch (e)
|
|
259
|
+
{
|
|
260
|
+
errors.push(e.message);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Reject unknown keys (prevent mass-assignment)
|
|
265
|
+
for (const key of Object.keys(data))
|
|
266
|
+
{
|
|
267
|
+
if (!columns[key])
|
|
268
|
+
{
|
|
269
|
+
errors.push(`Unknown column "${key}"`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return { valid: errors.length === 0, errors, sanitized };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// -- DDL Security Helpers --------------------------------
|
|
277
|
+
|
|
278
|
+
const VALID_FK_ACTIONS = new Set(['CASCADE', 'SET NULL', 'SET DEFAULT', 'RESTRICT', 'NO ACTION']);
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Validate and return a FK action string, or throw.
|
|
282
|
+
* @param {string} action - Foreign key action string (CASCADE, SET NULL, etc.).
|
|
283
|
+
* @returns {string} Uppercase validated action
|
|
284
|
+
*/
|
|
285
|
+
function validateFKAction(action)
|
|
286
|
+
{
|
|
287
|
+
const upper = String(action).toUpperCase();
|
|
288
|
+
if (!VALID_FK_ACTIONS.has(upper))
|
|
289
|
+
throw new Error(`Invalid FK action: "${action}". Allowed: ${[...VALID_FK_ACTIONS].join(', ')}`);
|
|
290
|
+
return upper;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Validate a CHECK expression for dangerous SQL patterns.
|
|
295
|
+
* @param {string} expr - SQL CHECK expression to validate.
|
|
296
|
+
* @returns {string} The original expression (validated).
|
|
297
|
+
*/
|
|
298
|
+
function validateCheck(expr)
|
|
299
|
+
{
|
|
300
|
+
const s = String(expr);
|
|
301
|
+
// Block semicolons, comment markers, and common injection patterns
|
|
302
|
+
if (/;|--|\bDROP\b|\bDELETE\b|\bINSERT\b|\bUPDATE\b|\bALTER\b|\bCREATE\b|\bEXEC\b/i.test(s))
|
|
303
|
+
throw new Error(`Potentially dangerous CHECK expression: "${s}"`);
|
|
304
|
+
return s;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
module.exports = { TYPES, validateValue, validate, validateFKAction, validateCheck };
|