Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/core/src/errors/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export enum ErrorCode {

KEY_VALUE_INVALID = 'keyValueInvalid',

KEY_VALIDATE_REJECTED = 'keyValidateRejected',

OPERATOR_UNSUPPORTED = 'operatorUnsupported',

FEATURE_UNSUPPORTED = 'featureUnsupported',
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/errors/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ export class ParseError extends BaseError {
});
}

static keyValidateRejected(key: string) {
return new this({
message: `The key ${key} was rejected by the schema validator.`,
code: ErrorCode.KEY_VALIDATE_REJECTED,
});
}

static operatorUnsupported(operator: string) {
return new this({
message: `The operator ${operator} is not supported.`,
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/parser/parameter/filters/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ export type FiltersParseOptions<
relations?: Relations,
schema?: string | Schema<RECORD> | FiltersSchema<RECORD>,
throwOnFailure?: boolean,
strict?: boolean
strict?: boolean,
context?: unknown,
};
18 changes: 12 additions & 6 deletions packages/core/src/parser/parameter/filters/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,17 @@ function isConditionValue(input: unknown) : input is ICondition {
export function applyFiltersSchemaValidation(
input: IFilter | IFilters,
schema: FiltersSchema,
context?: unknown,
) : IFilter | IFilters | undefined;
export function applyFiltersSchemaValidation(
input: ICondition,
schema: FiltersSchema,
context?: unknown,
) : ICondition | undefined;
export function applyFiltersSchemaValidation(
input: ICondition,
schema: FiltersSchema,
context?: unknown,
) : ICondition | undefined {
if (!schema.hasValidator()) {
return input;
Expand All @@ -83,7 +86,7 @@ export function applyFiltersSchemaValidation(
input.operator === FilterFieldOperator.ELEM_MATCH &&
isConditionValue(input.value)
) {
const interior = applyFiltersSchemaValidation(input.value, schema);
const interior = applyFiltersSchemaValidation(input.value, schema, context);
if (!interior) {
return undefined;
}
Expand All @@ -93,7 +96,7 @@ export function applyFiltersSchemaValidation(
}
}

const output = schema.validate(leaf);
const output = schema.validate(leaf, context);
if (isPromiseLike(output)) {
void Promise.resolve(output).catch(() => undefined);
throw SchemaError.validatorAsyncRequiresAsyncParser();
Expand All @@ -108,7 +111,7 @@ export function applyFiltersSchemaValidation(

const conditions : ICondition[] = [];
for (const child of input.value) {
const validated = applyFiltersSchemaValidation(child, schema);
const validated = applyFiltersSchemaValidation(child, schema, context);
if (validated) {
conditions.push(validated);
}
Expand All @@ -129,14 +132,17 @@ export function applyFiltersSchemaValidation(
export function applyFiltersSchemaValidationAsync(
input: IFilter | IFilters,
schema: FiltersSchema,
context?: unknown,
) : Promise<IFilter | IFilters | undefined>;
export function applyFiltersSchemaValidationAsync(
input: ICondition,
schema: FiltersSchema,
context?: unknown,
) : Promise<ICondition | undefined>;
export async function applyFiltersSchemaValidationAsync(
input: ICondition,
schema: FiltersSchema,
context?: unknown,
) : Promise<ICondition | undefined> {
if (!schema.hasValidator()) {
return input;
Expand All @@ -148,7 +154,7 @@ export async function applyFiltersSchemaValidationAsync(
input.operator === FilterFieldOperator.ELEM_MATCH &&
isConditionValue(input.value)
) {
const interior = await applyFiltersSchemaValidationAsync(input.value, schema);
const interior = await applyFiltersSchemaValidationAsync(input.value, schema, context);
if (!interior) {
return undefined;
}
Expand All @@ -158,7 +164,7 @@ export async function applyFiltersSchemaValidationAsync(
}
}

return (await schema.validate(leaf)) || undefined;
return (await schema.validate(leaf, context)) || undefined;
}

if (!isFilters(input)) {
Expand All @@ -167,7 +173,7 @@ export async function applyFiltersSchemaValidationAsync(

const conditions : ICondition[] = [];
for (const child of input.value) {
const validated = await applyFiltersSchemaValidationAsync(child, schema);
const validated = await applyFiltersSchemaValidationAsync(child, schema, context);
if (validated) {
conditions.push(validated);
}
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/parser/parameter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from './filters';
export * from './pagination';
export * from './relations';
export * from './sort';
export * from './validate';
3 changes: 2 additions & 1 deletion packages/core/src/parser/parameter/relations/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ export type RelationsParseOptions<
> = {
throwOnFailure?: boolean,
strict?: boolean,
schema?: string | Schema<RECORD> | RelationsSchema<RECORD>
schema?: string | Schema<RECORD> | RelationsSchema<RECORD>,
context?: unknown,
};
3 changes: 2 additions & 1 deletion packages/core/src/parser/parameter/sort/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ export type SortParseOptions<
relations?: Relations,
throwOnFailure?: boolean,
strict?: boolean,
schema?: string | Schema<RECORD> | SortSchema<RECORD>
schema?: string | Schema<RECORD> | SortSchema<RECORD>,
context?: unknown,
};
Binary file added packages/core/src/parser/parameter/validate.ts
Binary file not shown.
4 changes: 4 additions & 0 deletions packages/core/src/parser/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ export abstract class BaseQueryParser extends BaseParser<ParseQueryOptions, Quer
parameterOptions.strict = options.strict;
}

if (typeof options.context !== 'undefined') {
parameterOptions.context = options.context;
}

return { data, parameterOptions };
}

Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/parser/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ export type ParseParameterOptions<
schema?: Schema<RECORD> | string,
relations?: Relations,
strict?: boolean,
/**
* Caller-defined context forwarded to the schema validate hooks
* (relations/fields/sort key validators, filters leaf validator).
* Opaque to the parser.
*/
context?: unknown,
};

export type ParseQueryOptions<
Expand Down Expand Up @@ -44,6 +50,14 @@ export type ParseQueryOptions<
* every client key instead of falling back to the syntactic name check.
*/
strict?: boolean,
/**
* Caller-defined context (e.g. the authenticated actor) forwarded to
* every schema validate hook this parse run invokes. Hooks receive
* `undefined` when no context is supplied. Opaque to the parser;
* typing happens at the schema definition site
* (`defineSchema<RECORD, CONTEXT>`).
*/
context?: unknown,
};

export type IParserOptions = {
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/schema/define.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { ObjectLiteral } from '../types';

export function defineSchema<
RECORD extends ObjectLiteral = ObjectLiteral,
>(options: SchemaOptions<RECORD> = {}) : Schema<RECORD> {
CONTEXT = any,
>(options: SchemaOptions<RECORD, CONTEXT> = {}) : Schema<RECORD, CONTEXT> {
return new Schema(options);
}
13 changes: 7 additions & 6 deletions packages/core/src/schema/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,21 @@ import { BaseSchema } from './base';

export class Schema<
RECORD extends ObjectLiteral = ObjectLiteral,
> extends BaseSchema<SchemaOptions<RECORD>> {
public readonly fields : FieldsSchema<RECORD>;
CONTEXT = any,
> extends BaseSchema<SchemaOptions<RECORD, CONTEXT>> {
public readonly fields : FieldsSchema<RECORD, CONTEXT>;

public readonly filters : FiltersSchema<RECORD>;
public readonly filters : FiltersSchema<RECORD, CONTEXT>;

public readonly pagination : PaginationSchema;

public readonly relations: RelationsSchema<RECORD>;
public readonly relations: RelationsSchema<RECORD, CONTEXT>;

public readonly sort: SortSchema<RECORD>;
public readonly sort: SortSchema<RECORD, CONTEXT>;

// ---------------------------------------------------------

constructor(options: SchemaOptions<RECORD> = {}) {
constructor(options: SchemaOptions<RECORD, CONTEXT> = {}) {
super(options);

if (options.fields instanceof FieldsSchema) {
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/schema/parameter/fields/define.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { FieldsSchema } from './schema';

export function defineFieldsSchema<
RECORD extends ObjectLiteral = ObjectLiteral,
>(options: FieldsOptions<RECORD> = {}) : FieldsSchema<RECORD> {
CONTEXT = any,
>(options: FieldsOptions<RECORD, CONTEXT> = {}) : FieldsSchema<RECORD, CONTEXT> {
return new FieldsSchema(options);
}
18 changes: 16 additions & 2 deletions packages/core/src/schema/parameter/fields/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* view the LICENSE file that was distributed with this source code.
*/

import type { ObjectLiteral, SimpleKeys } from '../../../types';
import type { MaybeAsync, ObjectLiteral, SimpleKeys } from '../../../types';
import {
isPropertyNameValid,
} from '../../../utils';
Expand All @@ -14,7 +14,7 @@ import { BaseSchema } from '../../base';

export class FieldsSchema<
RECORD extends ObjectLiteral = ObjectLiteral,
CONTEXT extends ObjectLiteral = ObjectLiteral,
CONTEXT = any,
> extends BaseSchema<FieldsOptions<RECORD, CONTEXT>> {
public default : string[];

Expand Down Expand Up @@ -91,6 +91,20 @@ export class FieldsSchema<
return !this.defaultIsUndefined && this.default.length > 0;
}

// ---------------------------------------------------------

hasValidator() {
return typeof this.options.validate !== 'undefined';
}

validate(name: string, context: CONTEXT) : MaybeAsync<boolean | undefined> {
if (typeof this.options.validate === 'undefined') {
return true;
}

return this.options.validate(name, context);
}

/**
* Check whether a name exists for a group.
*
Expand Down
14 changes: 10 additions & 4 deletions packages/core/src/schema/parameter/fields/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,21 @@
* view the LICENSE file that was distributed with this source code.
*/

import type { BaseSchemaOptions, VerifyFn } from '../../types';
import type { ObjectLiteral, SimpleKeys } from '../../../types';
import type { BaseSchemaOptions, KeyValidator } from '../../types';
import type { SimpleKeys } from '../../../types';

export type FieldsOptions<
T extends Record<string, any> = Record<string, any>,
CONTEXT extends ObjectLiteral = ObjectLiteral,
CONTEXT = any,
> = BaseSchemaOptions & {
mapping?: Record<string, string>,
allowed?: SimpleKeys<T>[],
default?: SimpleKeys<T>[],
verify?: VerifyFn<string[], CONTEXT>
/**
* Dynamic per-field gate, e.g. an actor permission check.
* Runs once per client-requested field against the schema that
* governs it (the target schema for dotted keys). Schema defaults
* bypass the hook.
*/
validate?: KeyValidator<CONTEXT>,
};
3 changes: 2 additions & 1 deletion packages/core/src/schema/parameter/filters/define.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { FiltersSchema } from './schema';

export function defineFiltersSchema<
RECORD extends ObjectLiteral = ObjectLiteral,
>(options: FiltersOptions<RECORD> = {}) : FiltersSchema<RECORD> {
CONTEXT = any,
>(options: FiltersOptions<RECORD, CONTEXT> = {}) : FiltersSchema<RECORD, CONTEXT> {
return new FiltersSchema(options);
}
7 changes: 4 additions & 3 deletions packages/core/src/schema/parameter/filters/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import { BaseSchema } from '../../base';

export class FiltersSchema<
T extends ObjectLiteral = ObjectLiteral,
> extends BaseSchema<FiltersOptions<T>> {
CONTEXT = any,
> extends BaseSchema<FiltersOptions<T, CONTEXT>> {
public default : ICondition | undefined;

public defaultIsUndefined : boolean;
Expand Down Expand Up @@ -61,12 +62,12 @@ export class FiltersSchema<
return typeof this.options.validate !== 'undefined';
}

validate(input: IFilter) : MaybeAsync<IFilter | undefined> {
validate(input: IFilter, context: CONTEXT) : MaybeAsync<IFilter | undefined> {
if (typeof this.options.validate === 'undefined') {
return input;
}

return this.options.validate(input);
return this.options.validate(input, context);
}

// ---------------------------------------------------------
Expand Down
12 changes: 10 additions & 2 deletions packages/core/src/schema/parameter/filters/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,24 @@ import type { BaseSchemaOptions } from '../../types';
* `return input` — a bare block body would reject every filter. The result
* may also be a Promise of any of those values; resolving it requires the
* `parseAsync()` / `decodeAsync()` / `encodeAsync()` entry points.
*
* The second argument is the value passed to `parse()` / `decode()` via
* the `context` option (`undefined` when the caller supplied none), so a
* shared schema can make per-request decisions (e.g. actor permissions).
*/
export type Validator = (input: IFilter) => MaybeAsync<IFilter | undefined>;
export type Validator<CONTEXT = any> = (
input: IFilter,
context: CONTEXT,
) => MaybeAsync<IFilter | undefined>;

export type FiltersOptions<
T extends ObjectLiteral = ObjectLiteral,
CONTEXT = any,
> = BaseSchemaOptions & {
mapping?: Record<string, string>,
allowed?: SimpleKeys<T>[],
default?: ICondition,
validate?: Validator,
validate?: Validator<CONTEXT>,
/**
* Field keys whose equality comparisons (eq/ne/in/nin) stay
* case-sensitive instead of the case-insensitive default —
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/schema/parameter/relations/define.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import type { RelationsOptions } from './types';

export function defineRelationsSchema<
T extends ObjectLiteral= ObjectLiteral,
CONTEXT = any,
>(
options: RelationsOptions<T> = {},
) : RelationsSchema<T> {
options: RelationsOptions<T, CONTEXT> = {},
) : RelationsSchema<T, CONTEXT> {
return new RelationsSchema(options);
}
17 changes: 15 additions & 2 deletions packages/core/src/schema/parameter/relations/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,31 @@
* view the LICENSE file that was distributed with this source code.
*/

import type { ObjectLiteral } from '../../../types';
import type { MaybeAsync, ObjectLiteral } from '../../../types';
import { BaseSchema } from '../../base';
import type { RelationsOptions } from './types';

export class RelationsSchema<
T extends ObjectLiteral = ObjectLiteral,
> extends BaseSchema<RelationsOptions<T>> {
CONTEXT = any,
> extends BaseSchema<RelationsOptions<T, CONTEXT>> {
get allowed() {
return this.options.allowed;
}

get mapping() {
return this.options.mapping || {};
}

hasValidator() {
return typeof this.options.validate !== 'undefined';
}

validate(name: string, context: CONTEXT) : MaybeAsync<boolean | undefined> {
if (typeof this.options.validate === 'undefined') {
return true;
}

return this.options.validate(name, context);
}
}
Loading
Loading