Skip to content

Commit 75151b4

Browse files
committed
fix: finalize schema dsl runtime hardening
1 parent 54e81f6 commit 75151b4

14 files changed

Lines changed: 762 additions & 438 deletions

scripts/build-p1.cjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ async function main() {
4747
format: 'cjs',
4848
target: 'node18',
4949
sourcemap: emitSourceMaps,
50+
define: {
51+
MONSQLIZE_IMPORT_META_URL: 'undefined',
52+
},
5053
logLevel: 'info',
5154
});
5255

@@ -59,6 +62,9 @@ async function main() {
5962
format: 'esm',
6063
target: 'node18',
6164
sourcemap: emitSourceMaps,
65+
define: {
66+
MONSQLIZE_IMPORT_META_URL: 'import.meta.url',
67+
},
6268
logLevel: 'info',
6369
});
6470

src/capabilities/model/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,11 @@ export class ModelInstance<TDocument = Record<string, unknown>> {
680680
return this.runModelWrite(() => saveModelDocument(this.collection, document, {
681681
timestampsConfig: this._timestampsConfig,
682682
versionConfig: this._versionConfig,
683+
schemaValidationContext: {
684+
validateEnabled: this._validateEnabled,
685+
schemaCache: this._schemaCache,
686+
schemaValidateFn: this._schemaValidateFn,
687+
},
683688
nowFactory: () => this.nowDate(),
684689
}));
685690
}

src/capabilities/model/model-instance-helpers.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import {
2222
applyModelReplaceVersion,
2323
assertModelOptimisticLockMatched,
2424
assertNumericExpectedVersion,
25+
validateModelSchemaPayload,
26+
type ModelSchemaValidationContext,
2527
type ModelTimestampConfig,
2628
type ModelVersionConfig,
2729
} from './model-write-helpers';
@@ -353,6 +355,7 @@ export async function saveModelDocument<TDocument>(
353355
options: {
354356
timestampsConfig?: ModelTimestampConfig;
355357
versionConfig?: ModelVersionConfig;
358+
schemaValidationContext?: ModelSchemaValidationContext;
356359
nowFactory?: () => Date;
357360
} = {},
358361
): Promise<TDocument & Record<string, unknown>> {
@@ -366,6 +369,9 @@ export async function saveModelDocument<TDocument>(
366369
options.versionConfig,
367370
expectedVersion,
368371
) as Record<string, unknown>;
372+
if (options.schemaValidationContext) {
373+
validateModelSchemaPayload(options.schemaValidationContext, replacement);
374+
}
369375
const result = await collection.replaceOne(
370376
{ _id: payload._id, [options.versionConfig.field]: expectedVersion },
371377
replacement,
@@ -375,13 +381,19 @@ export async function saveModelDocument<TDocument>(
375381
Object.assign(document, replacement);
376382
return document;
377383
}
384+
if (options.schemaValidationContext) {
385+
validateModelSchemaPayload(options.schemaValidationContext, payload);
386+
}
378387
await collection.replaceOne({ _id: payload._id }, payload, { upsert: true });
379388
return document;
380389
}
381390
payload = applyModelInsertVersion(
382391
applyModelInsertTimestamps(payload, options.timestampsConfig ?? null, nowFactory),
383392
options.versionConfig ?? null,
384393
);
394+
if (options.schemaValidationContext) {
395+
validateModelSchemaPayload(options.schemaValidationContext, payload);
396+
}
385397
const result = await collection.insertOne(payload);
386398
Object.assign(document, payload);
387399
(document as Record<string, unknown>)._id = result.insertedId;

src/capabilities/model/model-mutation-orchestrator.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,11 @@ export async function orchestrateModelReplaceOne<TDocument = Record<string, unkn
445445
context.versionConfig,
446446
lock.expectedVersion,
447447
);
448+
validateModelSchemaPayload({
449+
validateEnabled: context.validateEnabled,
450+
schemaCache: context.schemaCache,
451+
schemaValidateFn: context.schemaValidateFn,
452+
}, nextReplacement as Record<string, unknown>, lock.driverOptions as Record<string, unknown> | undefined);
448453
const result = await context.collection.replaceOne(lock.filter, nextReplacement, lock.driverOptions);
449454
assertModelOptimisticLockMatched(result, context.versionConfig);
450455
if (context.hooksFactory) {
@@ -500,6 +505,11 @@ export async function orchestrateModelFindOneAndReplace<TDocument = Record<strin
500505
context.versionConfig,
501506
lock.expectedVersion,
502507
);
508+
validateModelSchemaPayload({
509+
validateEnabled: context.validateEnabled,
510+
schemaCache: context.schemaCache,
511+
schemaValidateFn: context.schemaValidateFn,
512+
}, nextReplacement as Record<string, unknown>, lock.driverOptions as Record<string, unknown> | undefined);
503513
const result = await context.extendedCollection().findOneAndReplace(lock.filter, nextReplacement, lock.driverOptions);
504514
assertModelOptimisticLockDocument(result, context.versionConfig);
505515
if (context.hooksFactory) {

src/capabilities/model/model-registry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { validateCollectionName, validateDefinition, processTimestamps } from '.
2222
* Static model registry.
2323
*
2424
* Typical usage:
25-
* Model.define('users', { schema: (dsl) => dsl({ name: 'string!' }) });
25+
* Model.define('users', { schema: (s) => s({ name: 'string!' }) });
2626
* const registered = Model.get('users');
2727
*/
2828
export class Model {

src/capabilities/model/model-write-helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ type ModelSchemaValidationResult = {
4242

4343
export type ModelSchemaValidateFn = ((schema: unknown, document: unknown) => ModelSchemaValidationResult) | null;
4444

45-
type ModelSchemaValidationContext = {
45+
export type ModelSchemaValidationContext = {
4646
validateEnabled: boolean;
4747
schemaCache: unknown;
4848
schemaValidateFn: ModelSchemaValidateFn;

src/capabilities/model/schema-dsl-function-scopes.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { findLocalDeclarationEnd } from './schema-dsl-local-declarations';
22

3-
const functionIdentifierPattern = /[A-Za-z_$][\w$]*/g;
3+
const functionIdentifierPattern = /[$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*/gu;
4+
const simpleFunctionIdentifierPattern = /^[$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*$/u;
5+
const functionIdentifierPartPattern = /[$\u200c\u200d\p{ID_Continue}]/u;
46

57
export type FunctionScopeInfo = {
68
start: number;
@@ -19,7 +21,7 @@ function addIdentifiersFromPattern(pattern: string, identifiers: Set<string>): v
1921
}
2022

2123
function isIdentifierPart(char: string | undefined): boolean {
22-
return char !== undefined && /[A-Za-z_$\d]/.test(char);
24+
return char !== undefined && functionIdentifierPartPattern.test(char);
2325
}
2426

2527
function splitTopLevel(source: string, delimiter: string): string[] {
@@ -114,7 +116,7 @@ function isIndexInRanges(index: number, ranges: ReadonlyArray<{ start: number; e
114116
function addBindingIdentifiers(pattern: string, identifiers: Set<string>): void {
115117
let binding = stripTopLevelDefault(pattern).replace(/^\s*\.\.\./, '').trim();
116118
if (!binding) return;
117-
if (/^[A-Za-z_$][\w$]*$/.test(binding)) {
119+
if (simpleFunctionIdentifierPattern.test(binding)) {
118120
identifiers.add(binding);
119121
return;
120122
}
@@ -314,7 +316,7 @@ function getClassMethodNameBinding(header: string, headerStart: number): Functio
314316
if (remaining.startsWith('[')) {
315317
return null;
316318
}
317-
const match = /^#?([A-Za-z_$][\w$]*)\s*\(/.exec(remaining);
319+
const match = /^#?([$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*)\s*\(/u.exec(remaining);
318320
if (!match) {
319321
return null;
320322
}
@@ -335,7 +337,7 @@ function stripClassMethodModifiers(header: string): string {
335337

336338
function isClassMethodHeader(header: string): boolean {
337339
const remaining = stripClassMethodModifiers(header);
338-
return remaining.startsWith('[') || /^#?[A-Za-z_$][\w$]*\s*\(/.test(remaining);
340+
return remaining.startsWith('[') || /^#?[$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*\s*\(/u.test(remaining);
339341
}
340342

341343
function getTrailingClassMethodHeader(header: string): { header: string; offset: number } | null {
@@ -357,7 +359,7 @@ function isObjectMethodHeader(header: string): boolean {
357359
}
358360
return remaining.startsWith('(')
359361
|| remaining.startsWith('[')
360-
|| /^(?:\d+(?:\.\d+)?|#?[A-Za-z_$][\w$]*)\s*\(/.test(remaining);
362+
|| /^(?:\d+(?:\.\d+)?|#?[$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*)\s*\(/u.test(remaining);
361363
}
362364

363365
function includeMethodModifiers(source: string, start: number): number {
@@ -424,7 +426,7 @@ function getClassFieldNameBinding(header: string, headerStart: number): Function
424426
if (remaining.startsWith('[')) {
425427
return null;
426428
}
427-
const match = /^#?([A-Za-z_$][\w$]*)/.exec(remaining);
429+
const match = /^#?([$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*)/u.exec(remaining);
428430
if (!match) {
429431
return null;
430432
}
@@ -478,7 +480,7 @@ function isClassDeclarationBinding(source: string, classIndex: number): boolean
478480

479481
function findClassMemberScopes(source: string): FunctionScopeInfo[] {
480482
const scopes: FunctionScopeInfo[] = [];
481-
for (const match of source.matchAll(/\bclass(?:\s+([A-Za-z_$][\w$]*))?/g)) {
483+
for (const match of source.matchAll(/\bclass(?:\s+([$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*))?/gu)) {
482484
const classIndex = match.index ?? 0;
483485
const className = match[1];
484486
if (className) {
@@ -666,7 +668,7 @@ export function addScopedLocalIdentifiers(
666668
}
667669
declarationPattern.lastIndex = Math.max(declarationPattern.lastIndex, declarationEnd);
668670
}
669-
for (const match of scopeSource.matchAll(/\bclass\s+([A-Za-z_$][\w$]*)/g)) {
671+
for (const match of scopeSource.matchAll(/\bclass\s+([$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*)/gu)) {
670672
const matchIndex = start + (match.index ?? 0);
671673
if (isIndexInRanges(matchIndex, excludedRanges)) {
672674
continue;
@@ -711,7 +713,7 @@ export function findNestedFunctionScopes(source: string, rootArrowIndex: number,
711713
const bodyEnd = findMatchingBrace(source, bodyStart);
712714
if (bodyEnd > bodyStart) {
713715
const header = source.slice(functionIndex, bodyStart);
714-
const functionMatch = /function(?:\s+([A-Za-z_$][\w$]*))?\s*\(([^)]*)\)/.exec(header);
716+
const functionMatch = /function(?:\s+([$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*))?\s*\(([^)]*)\)/u.exec(header);
715717
const boundIdentifiers = new Set<string>();
716718
if (functionMatch?.[1]) {
717719
boundIdentifiers.add(functionMatch[1]);

0 commit comments

Comments
 (0)