diff --git a/src/typeEvaluator/evaluateQueryType.ts b/src/typeEvaluator/evaluateQueryType.ts index 33969f7a..711a4700 100644 --- a/src/typeEvaluator/evaluateQueryType.ts +++ b/src/typeEvaluator/evaluateQueryType.ts @@ -29,7 +29,7 @@ import type { import {parse} from '../parser' import {handleFuncCallNode} from './functions' import {optimizeUnions} from './optimizations' -import {createContext, createScope, Scope} from './scope' +import {Context, Scope} from './scope' import type { ArrayTypeNode, BooleanTypeNode, @@ -57,7 +57,7 @@ const $warn = debug('typeEvaluator:evaluate::warn') export function typeEvaluate(ast: ExprNode, schema: Schema): TypeNode { const parsed = walk({ node: ast, - scope: createScope([], undefined, createContext(schema)), + scope: new Scope([], undefined, new Context(schema)), }) $trace('evaluateQueryType.parsed %O', parsed) @@ -352,7 +352,7 @@ function handleSelectNode(node: SelectNode, scope: Scope): TypeNode { const conditionValue = walk({node: alternative.condition, scope}) const conditionScope = resolveFilter(alternative.condition, scope) if (conditionScope.type === 'union' && conditionScope.of.length > 0) { - values.push(walk({node: alternative.value, scope: scope.subscope(conditionScope.of, true)})) + values.push(walk({node: alternative.value, scope: scope.createHidden(conditionScope.of)})) } if (conditionValue.type === 'boolean' && conditionValue.value === true) { guaranteed = true @@ -388,7 +388,7 @@ function handleFlatMap(node: FlatMapNode, scope: Scope): TypeNode { return {type: 'null'} } - return walk({node: node.expr, scope: scope.subscope([base.of], true)}) + return walk({node: node.expr, scope: scope.createHidden([base.of])}) }) } function handleMap(node: MapNode, scope: Scope): TypeNode { @@ -400,7 +400,7 @@ function handleMap(node: MapNode, scope: Scope): TypeNode { } if (base.of.type === 'union') { - const value = walk({node: node.expr, scope: scope.subscope(base.of.of, true)}) // re use the current parent, this is a "sub" scope + const value = walk({node: node.expr, scope: scope.createHidden(base.of.of)}) // re use the current parent, this is a "sub" scope $trace('map.expr %O', value) return { @@ -451,7 +451,7 @@ function handleProjectionNode(node: ProjectionNode, scope: Scope): TypeNode { } return walk({ node: node.expr, - scope: scope.subscope([field]), + scope: scope.createNested([field]), }) }) } @@ -459,12 +459,12 @@ function handleProjectionNode(node: ProjectionNode, scope: Scope): TypeNode { function createFilterScope(base: TypeNode, scope: Scope): Scope { if (base.type === 'array') { if (base.of.type === 'union') { - return scope.subscope(base.of.of) + return scope.createNested(base.of.of) } - return scope.subscope([base.of]) + return scope.createNested([base.of]) } - return scope.subscope([base]) + return scope.createNested([base]) } function handleFilterNode(node: FilterNode, scope: Scope): TypeNode { const base = walk({node: node.base, scope}) @@ -612,16 +612,20 @@ function handleValueNode(node: ValueNode, scope: Scope): TypeNode { } } -function handleParentNode(node: ParentNode, scope: Scope): TypeNode { - let newScope: Scope | undefined = scope - for (let n = node.n; n > 0; n--) { - newScope = newScope?.parent +function handleParentNode({n}: ParentNode, scope: Scope): TypeNode { + let current: Scope = scope + for (let i = 0; i < n; i++) { + if (!current.parent) { + return {type: 'null'} satisfies NullTypeNode + } + current = current.parent } - $trace('parent.scope %d %O', node.n, newScope) - if (newScope !== undefined) { - return newScope.value + $trace('parent.scope %d %O', n, current.value) + if (current.value.of.length === 0) { + return {type: 'null'} satisfies NullTypeNode } - return {type: 'null'} satisfies NullTypeNode + + return current.value } function handleNotNode(node: NotNode, scope: Scope): TypeNode { @@ -1066,7 +1070,7 @@ function resolveFilter(expr: ExprNode, scope: Scope): TypeNode { (node) => // create a new scope with the current scopes parent as the parent. It's only a temporary scope since we only want to resolve the condition // check if the result is true or undefined. Undefined means that the condition can't be resolved, and we should keep the node - resolveCondition(expr, scope.subscope([node], true)) !== false, + resolveCondition(expr, scope.createHidden([node])) !== false, ) $trace( `resolveFilter ${expr.type === 'OpCall' ? `${expr.type}/${expr.op}` : expr.type} %O`, diff --git a/src/typeEvaluator/scope.ts b/src/typeEvaluator/scope.ts index 40214bd1..a16e96a4 100644 --- a/src/typeEvaluator/scope.ts +++ b/src/typeEvaluator/scope.ts @@ -5,63 +5,62 @@ import {NullTypeNode, ReferenceTypeNode, Schema, TypeNode, UnionTypeNode} from ' const $trace = debug('typeEvaluator:scope:trace') $trace.log = console.log.bind(console) // eslint-disable-line no-console -export interface Context { +export class Context { readonly schema: Schema - lookupRef(ref: ReferenceTypeNode): TypeNode - lookupType(name: ReferenceTypeNode): TypeNode -} - -export function createContext(schema: Schema): Context { - return { - schema, + constructor(schema: Schema) { + this.schema = schema + } - lookupRef(ref) { - for (const val of this.schema) { - if (val.type === 'document') { - if (val.name === ref.to) { - return { - type: 'object', - attributes: val.attributes, - } + lookupRef(ref: ReferenceTypeNode): TypeNode { + for (const val of this.schema) { + if (val.type === 'document') { + if (val.name === ref.to) { + return { + type: 'object', + attributes: val.attributes, } } } - return {type: 'null'} satisfies NullTypeNode - }, + } + return {type: 'null'} satisfies NullTypeNode + } - lookupType(ref) { - for (const val of this.schema) { - if (val.type === 'type') { - if (val.name === ref.to) { - return val.value - } + lookupType(ref: ReferenceTypeNode): TypeNode { + for (const val of this.schema) { + if (val.type === 'type') { + if (val.name === ref.to) { + return val.value } } - return {type: 'null'} satisfies NullTypeNode - }, + } + return {type: 'null'} satisfies NullTypeNode } } -export interface Scope { - value: UnionTypeNode - parent: Scope | undefined - context: Context +export class Scope { + public value: UnionTypeNode + public parent: Scope | undefined + public context: Context + public isHidden: boolean - subscope(value: TypeNode[], hidden?: boolean): Scope -} -export function createScope(value: TypeNode[], parent?: Scope, context?: Context): Scope { - $trace('createScope', JSON.stringify({value, hasParent: parent !== undefined}, null, 2)) - return { - value: {type: 'union', of: value} satisfies UnionTypeNode, - parent, - context: context || parent?.context || createContext([]), + constructor(value: TypeNode[], parent?: Scope, context?: Context) { + this.value = {type: 'union', of: value} satisfies UnionTypeNode + this.parent = parent + this.context = context || parent?.context || new Context([]) + this.isHidden = false + } - subscope(value, hidden = false) { - if (hidden) { - return createScope(value, this.parent, this.context) - } - return createScope(value, this, this.context) - }, + createNested(value: TypeNode[]): Scope { + if (this.isHidden) { + return new Scope(value, this.parent, this.context) + } + return new Scope(value, this, this.context) + } + + createHidden(value: TypeNode[]): Scope { + const result = this.createNested(value) + result.isHidden = true + return result } } diff --git a/tap-snapshots/test/evaluateQueryType.test.ts.test.cjs b/tap-snapshots/test/evaluateQueryType.test.ts.test.cjs index 67163bab..c24b4ad3 100644 --- a/tap-snapshots/test/evaluateQueryType.test.ts.test.cjs +++ b/tap-snapshots/test/evaluateQueryType.test.ts.test.cjs @@ -643,6 +643,12 @@ exports[`test/evaluateQueryType.test.ts TAP filter order doesnt matter > must ma Object { "of": Object { "attributes": Object { + "_createdAt": Object { + "type": "objectAttribute", + "value": Object { + "type": "string", + }, + }, "_id": Object { "type": "objectAttribute", "value": Object { @@ -656,6 +662,12 @@ Object { "value": "author", }, }, + "age": Object { + "type": "objectAttribute", + "value": Object { + "type": "number", + }, + }, "firstname": Object { "type": "objectAttribute", "value": Object { diff --git a/test/evaluateQueryType.test.ts b/test/evaluateQueryType.test.ts index 748423a1..e7b6b726 100644 --- a/test/evaluateQueryType.test.ts +++ b/test/evaluateQueryType.test.ts @@ -11,7 +11,6 @@ import { TypeNode, UnionTypeNode, } from '../src/typeEvaluator/types' -import {satisfies} from '../src/typeEvaluator/satisfies' const postDocument = { type: 'document', @@ -43,6 +42,13 @@ const postDocument = { }, optional: true, } satisfies ObjectAttribute, + publishedAt: { + type: 'objectAttribute', + value: { + type: 'string', + }, + optional: true, + } satisfies ObjectAttribute, author: { type: 'objectAttribute', value: { @@ -133,6 +139,12 @@ const authorDocument = { type: 'string', }, }, + _createdAt: { + type: 'objectAttribute', + value: { + type: 'string', + }, + }, age: { type: 'objectAttribute', value: { @@ -820,6 +832,13 @@ t.test('deref with projection union', (t) => { }, optional: true, }, + publishedAt: { + type: 'objectAttribute', + value: { + type: 'string', + }, + optional: true, + }, author: { type: 'objectAttribute', value: { @@ -1318,6 +1337,18 @@ t.test('with splat', (t) => { type: 'string', }, }, + _createdAt: { + type: 'objectAttribute', + value: { + type: 'string', + }, + }, + age: { + type: 'objectAttribute', + value: { + type: 'number', + }, + }, object: { type: 'objectAttribute', value: {