Split up related field node type in data abstraction layer (#19942)

* Fix casing in file names

* Refactor file structure

* Split up related field node

* formatter

* renamed fields-node.ts to fields.ts

* Small grammar fix in comment

Co-authored-by: Jan Arends <jan.arends@mailbox.org>

---------

Co-authored-by: Jan Arends <jan.arends@mailbox.org>
Co-authored-by: Pascal Jufer <pascal-jufer@bluewin.ch>
This commit is contained in:
Nicola Krumschmidt
2023-10-09 16:12:42 +02:00
committed by GitHub
parent dfe7513f13
commit 6c45914b8a
29 changed files with 298 additions and 284 deletions

View File

@@ -125,7 +125,7 @@ describe('querying the driver', () => {
alias: secondFieldAlias,
},
{
type: 'm2o',
type: 'nested-one',
fields: [
{
type: 'primitive',
@@ -137,14 +137,17 @@ describe('querying the driver', () => {
field: joinField2,
},
],
join: {
current: {
fields: [fk],
},
external: {
store: 'randomDataStore1',
collection: randomCollectionToJoin,
fields: [foreignPk],
meta: {
type: 'm2o',
join: {
current: {
fields: [fk],
},
external: {
store: 'randomDataStore1',
collection: randomCollectionToJoin,
fields: [foreignPk],
},
},
},
alias: joinAlias,

View File

@@ -1,6 +1,6 @@
import { expect, test } from 'vitest';
import { randomIdentifier } from '@directus/random';
import type { AbstractQueryFieldNodeRelatedManyToOne } from '@directus/data';
import type { AbstractQueryFieldNodeRelationalManyToOne } from '@directus/data';
import { createJoin } from './create-join.js';
import type { AbstractSqlQueryJoinNode } from '../../types/clauses/joins/join.js';
@@ -10,10 +10,9 @@ test('Convert m2o relation on single field ', () => {
const randomExternalCollection = randomIdentifier();
const randomExternalStore = randomIdentifier();
const randomExternalField = randomIdentifier();
const randomExternalSelectField = randomIdentifier();
const randomAlias = randomIdentifier();
const node: AbstractQueryFieldNodeRelatedManyToOne = {
const node: AbstractQueryFieldNodeRelationalManyToOne = {
type: 'm2o',
join: {
current: {
@@ -25,12 +24,6 @@ test('Convert m2o relation on single field ', () => {
fields: [randomExternalField],
},
},
fields: [
{
type: 'primitive',
field: randomExternalSelectField,
},
],
};
const expected: AbstractSqlQueryJoinNode = {
@@ -68,11 +61,10 @@ test('Convert m2o relation with composite keys', () => {
const randomExternalStore = randomIdentifier();
const randomExternalField = randomIdentifier();
const randomExternalField2 = randomIdentifier();
const randomExternalSelectField = randomIdentifier();
const randomGeneratedAlias = randomIdentifier();
const randomUserAlias = randomIdentifier();
const node: AbstractQueryFieldNodeRelatedManyToOne = {
const node: AbstractQueryFieldNodeRelationalManyToOne = {
type: 'm2o',
join: {
current: {
@@ -84,13 +76,6 @@ test('Convert m2o relation with composite keys', () => {
fields: [randomExternalField, randomExternalField2],
},
},
fields: [
{
type: 'primitive',
field: randomExternalSelectField,
},
],
alias: randomUserAlias,
};
const expected: AbstractSqlQueryJoinNode = {
@@ -143,5 +128,5 @@ test('Convert m2o relation with composite keys', () => {
alias: randomUserAlias,
};
expect(createJoin(randomCurrentCollection, node, randomGeneratedAlias)).toStrictEqual(expected);
expect(createJoin(randomCurrentCollection, node, randomGeneratedAlias, randomUserAlias)).toStrictEqual(expected);
});

View File

@@ -1,11 +1,12 @@
import type { AbstractQueryFieldNodeRelatedManyToOne } from '@directus/data';
import type { AbstractQueryFieldNodeRelationalManyToOne } from '@directus/data';
import type { AbstractSqlQueryConditionNode, AbstractSqlQueryLogicalNode } from '../../types/index.js';
import type { AbstractSqlQueryJoinNode } from '../../types/index.js';
export const createJoin = (
currentCollection: string,
relationalField: AbstractQueryFieldNodeRelatedManyToOne,
externalCollectionAlias: string
relationalField: AbstractQueryFieldNodeRelationalManyToOne,
externalCollectionAlias: string,
fieldAlias?: string
): AbstractSqlQueryJoinNode => {
let on: AbstractSqlQueryLogicalNode | AbstractSqlQueryConditionNode;
@@ -40,8 +41,8 @@ export const createJoin = (
on,
};
if (relationalField.alias) {
result.alias = relationalField.alias;
if (fieldAlias) {
result.alias = fieldAlias;
}
return result;

View File

@@ -132,23 +132,26 @@ test('primitive, fn, m2o', () => {
field: randomPrimitiveField1,
},
{
type: 'm2o',
join: {
current: {
fields: [randomJoinCurrentField],
},
external: {
store: randomExternalStore,
collection: randomExternalCollection,
fields: [randomExternalField],
},
},
type: 'nested-one',
fields: [
{
type: 'primitive',
field: randomJoinNodeField,
},
],
meta: {
type: 'm2o',
join: {
current: {
fields: [randomJoinCurrentField],
},
external: {
store: randomExternalStore,
collection: randomExternalCollection,
fields: [randomExternalField],
},
},
},
},
{
type: 'fn',

View File

@@ -50,7 +50,7 @@ export const convertFieldNodes = (
continue;
}
if (abstractField.type === 'm2o') {
if (abstractField.type === 'nested-one') {
/**
* Always fetch the current context foreign key as well. We need it to check if the current
* item has a related item so we don't expand `null` values in a nested object where every
@@ -59,18 +59,20 @@ export const convertFieldNodes = (
* @TODO
*/
const m2oField = abstractField;
const externalCollectionAlias = createUniqueAlias(m2oField.join.external.collection);
const sqlJoinNode = createJoin(collection, m2oField, externalCollectionAlias);
if (abstractField.meta.type === 'm2o') {
const externalCollectionAlias = createUniqueAlias(abstractField.meta.join.external.collection);
const sqlJoinNode = createJoin(collection, abstractField.meta, externalCollectionAlias, abstractField.alias);
const nestedOutput = convertFieldNodes(externalCollectionAlias, abstractField.fields, idxGenerator, [
...currentPath,
abstractField.join.external.collection,
]);
const nestedOutput = convertFieldNodes(externalCollectionAlias, abstractField.fields, idxGenerator, [
...currentPath,
abstractField.meta.join.external.collection,
]);
nestedOutput.aliasMapping.forEach((value, key) => aliasRelationalMapping.set(key, value));
joins.push(sqlJoinNode);
select.push(...nestedOutput.clauses.select);
}
nestedOutput.aliasMapping.forEach((value, key) => aliasRelationalMapping.set(key, value));
joins.push(sqlJoinNode);
select.push(...nestedOutput.clauses.select);
continue;
}

View File

@@ -1,7 +1,9 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import type { AbstractQuery, DataDriver } from './index.js';
import { DataEngine } from './index.js';
import { randomIdentifier } from '@directus/random';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { DataEngine } from './engine.js';
import type { AbstractQuery } from './types/abstract-query.js';
import type { DataDriver } from './types/driver.js';
let engine: DataEngine;

View File

@@ -0,0 +1,39 @@
import type { ReadableStream } from 'node:stream/web';
import type { AbstractQuery } from './types/abstract-query.js';
import type { DataDriver } from './types/driver.js';
export class DataEngine {
#stores: Map<string, DataDriver>;
constructor() {
this.#stores = new Map();
}
/** Registers a new data store for use in queries */
async registerStore(name: string, driver: DataDriver) {
await driver.register?.();
this.#stores.set(name, driver);
}
/** Access the driver of a given store. Errors if it hasn't been registered */
store(name: string): DataDriver {
const store = this.#stores.get(name);
if (!store) {
throw new Error(`Store "${name}" doesn't exist.`);
}
return store;
}
/** Execute a root abstract query */
async query(query: AbstractQuery): Promise<ReadableStream> {
return this.store(query.store).query(query);
}
/** Gracefully shutdown connected drivers */
async destroy() {
await Promise.all(Array.from(this.#stores.values()).map((driver) => driver.destroy?.()));
}
}

View File

@@ -1,40 +1,2 @@
import type { ReadableStream } from 'node:stream/web';
import type { AbstractQuery } from './types/abstract-query/abstract-query.js';
import type { DataDriver } from './types/driver.js';
export class DataEngine {
#stores: Map<string, DataDriver>;
constructor() {
this.#stores = new Map();
}
/** Registers a new data store for use in queries */
async registerStore(name: string, driver: DataDriver) {
await driver.register?.();
this.#stores.set(name, driver);
}
/** Access the driver of a given store. Errors if it hasn't been registered */
store(name: string): DataDriver {
const store = this.#stores.get(name);
if (!store) {
throw new Error(`Store "${name}" doesn't exist.`);
}
return store;
}
/** Execute a root abstract query */
async query(query: AbstractQuery): Promise<ReadableStream> {
return this.store(query.store).query(query);
}
/** Gracefully shutdown connected drivers */
async destroy() {
await Promise.all(Array.from(this.#stores.values()).map((driver) => driver.destroy?.()));
}
}
export type * from './engine.js';
export type * from './types/index.js';

View File

@@ -4,8 +4,8 @@
*
* @module abstract-query
*/
import type { AbstractQueryModifiers } from './modifiers/index.js';
import type { AbstractQueryFieldNode } from './fields/fieldNodes.js';
import type { AbstractQueryFieldNode } from './abstract-query/fields/fields.js';
import type { AbstractQueryModifiers } from './abstract-query/modifiers.js';
/**
* The abstract root query
@@ -34,9 +34,3 @@ export interface AbstractQuery {
* @TODO
* - Rethink every / some
*/
export * from './modifiers/index.js';
export * from './fields/function.js';
export * from './fields/primitive.js';
export * from './fields/related.js';
export * from './fields/fieldNodes.js';

View File

@@ -1,8 +1,9 @@
import type { AbstractQueryFieldNodePrimitive } from './primitive.js';
import type { AbstractQueryFieldNodeFn } from './function.js';
import type { AbstractQueryFieldNodeRelated } from './related.js';
import type { AbstractQueryFieldNodeNestedMany, AbstractQueryFieldNodeNestedOne } from './nested.js';
import type { AbstractQueryFieldNodePrimitive } from './primitive.js';
export type AbstractQueryFieldNode =
| AbstractQueryFieldNodePrimitive
| AbstractQueryFieldNodeFn
| AbstractQueryFieldNodeRelated;
| AbstractQueryFieldNodeNestedMany
| AbstractQueryFieldNodeNestedOne;

View File

@@ -0,0 +1,5 @@
export * from './fields.js';
export * from './function.js';
export * from './nested.js';
export * from './nested/index.js';
export * from './primitive.js';

View File

@@ -0,0 +1,29 @@
import type { AbstractQueryModifiers } from '../modifiers.js';
import type { AbstractQueryFieldNode } from './fields.js';
import type {
AbstractQueryFieldNodeNestedRelationalMany,
AbstractQueryFieldNodeNestedRelationalOne,
} from './nested/relational.js';
export interface AbstractQueryFieldNodeNestedOne {
type: 'nested-one';
/* From the related collection the user can pick primitives, apply a function or add another nested node */
fields: AbstractQueryFieldNode[];
alias?: string;
meta: AbstractQueryFieldNodeNestedRelationalOne; // AbstractQueryFieldNodeNestedObjectOne | AbstractQueryFieldNodeNestedJsonOne
}
export interface AbstractQueryFieldNodeNestedMany {
type: 'nested-many';
/* From the related collection the user can pick primitives, apply a function or add another nested node */
fields: AbstractQueryFieldNode[];
alias?: string;
/** For many, it's always possible to add modifiers to the foreign collection to adjust the results. */
modifiers?: AbstractQueryModifiers;
meta: AbstractQueryFieldNodeNestedRelationalMany; // AbstractQueryFieldNodeNestedObjectMany | AbstractQueryFieldNodeNestedJsonMany
}

View File

@@ -0,0 +1 @@
export * from './relational.js';

View File

@@ -0,0 +1,77 @@
/**
* Used to build a relational query for m2o and a2o relations.
*/
export type AbstractQueryFieldNodeNestedRelationalOne =
| AbstractQueryFieldNodeRelationalManyToOne
| AbstractQueryFieldNodeRelationalAnyToOne;
/**
* Used to build a relational query for o2m and o2a relations.
*/
export type AbstractQueryFieldNodeNestedRelationalMany =
| AbstractQueryFieldNodeRelationalOneToMany
| AbstractQueryFieldNodeRelationalOneToAny;
export interface AbstractQueryFieldNodeRelationalManyToOne {
type: 'm2o';
join: AbstractQueryFieldNodeRelationalJoinMany;
}
export interface AbstractQueryFieldNodeRelationalOneToMany {
type: 'o2m';
join: AbstractQueryFieldNodeRelationalJoinMany;
}
export interface AbstractQueryFieldNodeRelationalAnyToOne {
type: 'a2o';
join: AbstractQueryFieldNodeRelationalJoinAny;
}
export interface AbstractQueryFieldNodeRelationalOneToAny {
type: 'o2a';
join: AbstractQueryFieldNodeRelationalJoinAny;
}
/**
* Used to build a relational query for m2o and o2m relations.
* @example
* ```
* const functionNode = {
* current: {
* fields: ['id']
* },
* external: {
* store: 'mongodb',
* collection: 'some-collection',
* }
* ```
*/
export interface AbstractQueryFieldNodeRelationalJoinMany {
/** the fields of the current collection which have the relational value to an external collection or item */
current: {
fields: [string, ...string[]];
};
/** the external collection or item which should be pulled/joined/merged into the current collection */
external: {
store?: string;
collection: string;
fields: [string, ...string[]];
};
}
export interface AbstractQueryFieldNodeRelationalJoinAny {
current: {
collectionField: string;
fields: [string, ...string[]];
};
external: {
store?: string;
fields: [string, ...string[]];
};
}

View File

@@ -1,110 +0,0 @@
/**
* This file will be split up into multiple files soon.
*/
import type { AbstractQueryModifiers } from '../modifiers/index.js';
import type { AbstractQueryFieldNodePrimitive } from './primitive.js';
import type { AbstractQueryFieldNodeFn } from './function.js';
/**
* This is a basic interface for all relational field types.
*/
export interface AbstractQueryFieldNodeRelatedBase {
/* From the related collection the user can pick primitives, apply a function or add another relational node */
fields: (AbstractQueryFieldNodePrimitive | AbstractQueryFieldNodeFn | AbstractQueryFieldNodeRelated)[];
/** Regardless of the type of the relationship, it always possible to add modifiers to the foreign collection to adjust the results. */
modifiers?: AbstractQueryModifiers;
alias?: string;
}
/**
* Used to build a relational query for m2o and o2m relations.
*/
export type AbstractQueryFieldNodeRelated =
| AbstractQueryFieldNodeRelatedManyToOne
| AbstractQueryFieldNodeRelatedOneToMany
| AbstractQueryFieldNodeRelatedAnyToOne
| AbstractQueryFieldNodeRelatedOneToAny;
/**
* Used to build a relational query for m2o and o2m relations.
* @example
* ```
* const functionNode = {
* current: {
* fields: ['id']
* },
* external: {
* store: 'mongodb',
* collection: 'some-collection',
* }
* ```
*/
export interface AbstractQueryFieldNodeRelatedJoinMany {
/** the field of the current collection which has the relational value to an external collection or item */
current: {
fields: [string, ...string[]];
};
/** the external collection or item which should be pulled/joined/merged into the current collection */
external: {
store?: string;
collection: string;
fields: [string, ...string[]];
};
}
export interface AbstractQueryFieldNodeRelatedJoinAny {
current: {
collectionField: string;
fields: [string, ...string[]];
};
external: {
store?: string;
fields: [string, ...string[]];
};
}
export interface AbstractQueryFieldNodeRelatedManyToOne extends AbstractQueryFieldNodeRelatedBase {
type: 'm2o';
join: AbstractQueryFieldNodeRelatedJoinMany;
}
export interface AbstractQueryFieldNodeRelatedOneToMany extends AbstractQueryFieldNodeRelatedBase {
type: 'o2m';
// maybe every here
join: AbstractQueryFieldNodeRelatedJoinMany;
}
export interface AbstractQueryFieldNodeRelatedAnyToOne extends AbstractQueryFieldNodeRelatedBase {
type: 'a2o';
join: AbstractQueryFieldNodeRelatedJoinAny;
}
export interface AbstractQueryFieldNodeRelatedOneToAny extends AbstractQueryFieldNodeRelatedBase {
type: 'o2a';
join: AbstractQueryFieldNodeRelatedJoinAny;
}
/**
* Continue on it after relationships
* @deprecated Those information will probably go within the o2m relational node
**/
export interface AbstractQueryQuantifierNode {
type: 'quantifier';
operator: 'every' | 'some';
/** The o2m field that the every/some should be applied on */
target: AbstractQueryFieldNodeRelatedOneToMany | AbstractQueryFieldNodeRelatedOneToAny;
/** An alias to reference the o2m item */
alias: string;
/** the values for the the operation. */
// childNode: AbstractQueryConditionNode | AbstractQueryNodeLogical | AbstractQueryNodeNegate;
}

View File

@@ -0,0 +1,3 @@
export * from './fields/index.js';
export * from './modifiers.js';
export * from './modifiers/index.js';

View File

@@ -0,0 +1,14 @@
import type { AbstractQueryFilterNode } from './modifiers/filters.js';
import type { AbstractQueryNodeLimit } from './modifiers/limit.js';
import type { AbstractQueryNodeOffset } from './modifiers/offset.js';
import type { AbstractQueryNodeSort } from './modifiers/sort.js';
/**
* Optional attributes to customize the query results
*/
export interface AbstractQueryModifiers {
limit?: AbstractQueryNodeLimit;
offset?: AbstractQueryNodeOffset;
sort?: AbstractQueryNodeSort[];
filter?: AbstractQueryFilterNode;
}

View File

@@ -0,0 +1,5 @@
import type { AbstractQueryConditionNode } from './filters/conditions.js';
import type { AbstractQueryNodeLogical } from './filters/logical.js';
import type { AbstractQueryNodeNegate } from './filters/negate.js';
export type AbstractQueryFilterNode = AbstractQueryConditionNode | AbstractQueryNodeLogical | AbstractQueryNodeNegate;

View File

@@ -0,0 +1,37 @@
import type { ConditionFieldNode } from './conditions/field-condition.js';
import type { ConditionGeoIntersectsBBoxNode } from './conditions/geo-condition-bbox.js';
import type { ConditionGeoIntersectsNode } from './conditions/geo-condition.js';
import type { ConditionNumberNode } from './conditions/number-condition.js';
import type { ConditionSetNode } from './conditions/set-condition.js';
import type { ConditionStringNode } from './conditions/string-condition.js';
/**
* Used to specify a condition on a query.
* Note: No explicit support to check for 'empty' (it's just an empty string) and null.
*
* @example
* ```
* {
* type: 'condition',
* condition: {...}
* },
* ```
*/
export interface AbstractQueryConditionNode {
type: 'condition';
condition: ActualConditionNodes;
}
/**
* Possible nodes which specify the condition.
*
* @todo The API should make sure, that the type of the targeting column has the correct type,
* so that f.e. a condition-string will only be applied to a column of type string.
*/
export type ActualConditionNodes =
| ConditionStringNode
| ConditionNumberNode
| ConditionGeoIntersectsNode
| ConditionGeoIntersectsBBoxNode
| ConditionSetNode
| ConditionFieldNode;

View File

@@ -1,4 +1,5 @@
import type { GeoJSONGeometryCollection, GeoJSONMultiPolygon, GeoJSONPolygon } from 'wellknown';
import type { AbstractQueryFieldNodePrimitive } from '../../../fields/primitive.js';
/**

View File

@@ -1,4 +1,3 @@
import type { AbstractQueryFieldNodePrimitive } from '../../../fields/primitive.js';
import type {
GeoJSONGeometryCollection,
GeoJSONLineString,
@@ -7,6 +6,8 @@ import type {
GeoJSONPoint,
} from 'wellknown';
import type { AbstractQueryFieldNodePrimitive } from '../../../fields/primitive.js';
/**
* Checks if a non box geo object intersects with another.
* @example

View File

@@ -1,44 +1,6 @@
import type { ConditionGeoIntersectsNode } from './geo-condition.js';
import type { ConditionGeoIntersectsBBoxNode } from './geo-condition-bbox.js';
import type { ConditionFieldNode } from './field-condition.js';
import type { ConditionNumberNode } from './number-condition.js';
import type { ConditionSetNode } from './set-condition.js';
import type { ConditionStringNode } from './string-condition.js';
/**
* Used to specify a condition on a query.
* Note: No explicit support to check for 'empty' (it's just an empty string) and null.
*
* @example
* ```
* {
* type: 'condition',
* condition: {...}
* },
* ```
*/
export interface AbstractQueryConditionNode {
type: 'condition';
condition: ActualConditionNodes;
}
/**
* Possible nodes which specify the condition.
*
* @todo The API should make sure, that the type of the targeting column has the correct type,
* so that f.e. a condition-string will only be applied to a column of type string.
*/
export type ActualConditionNodes =
| ConditionStringNode
| ConditionNumberNode
| ConditionGeoIntersectsNode
| ConditionGeoIntersectsBBoxNode
| ConditionSetNode
| ConditionFieldNode;
// Those need to be exported to be used solely in the corresponding converter
export * from './field-condition.js';
export * from './geo-condition-bbox.js';
export * from './geo-condition.js';
export * from './number-condition.js';
export * from './string-condition.js';
export * from './set-condition.js';
export * from './string-condition.js';

View File

@@ -1,9 +1,5 @@
import type { AbstractQueryConditionNode } from './conditions/index.js';
import type { AbstractQueryNodeLogical } from './logical.js';
import type { AbstractQueryNodeNegate } from './negate.js';
export * from './conditions.js';
export * from './conditions/index.js';
export * from './quantifier.js';
export * from './logical.js';
export * from './negate.js';
export type AbstractQueryFilterNode = AbstractQueryConditionNode | AbstractQueryNodeLogical | AbstractQueryNodeNegate;

View File

@@ -1,4 +1,4 @@
import type { AbstractQueryConditionNode } from './conditions/index.js';
import type { AbstractQueryConditionNode } from './conditions.js';
import type { AbstractQueryNodeNegate } from './negate.js';
/**

View File

@@ -1,4 +1,4 @@
import type { AbstractQueryFilterNode } from './index.js';
import type { AbstractQueryFilterNode } from '../filters.js';
/**
* Specifies that the wrapper filter should be negated.

View File

@@ -0,0 +1,13 @@
export interface AbstractQueryQuantifierNode {
type: 'quantifier';
operator: 'every' | 'some';
/** The o2m field that the every/some should be applied on */
target: string;
/** An alias to reference the o2m item */
alias: string;
/** the values for the the operation. */
// childNode: AbstractQueryConditionNode | AbstractQueryNodeLogical | AbstractQueryNodeNegate;
}

View File

@@ -1,19 +1,5 @@
import type { AbstractQueryFilterNode } from './filters/index.js';
import type { AbstractQueryNodeLimit } from './limit.js';
import type { AbstractQueryNodeOffset } from './offset.js';
import type { AbstractQueryNodeSort } from './sort.js';
/**
* Optional attributes to customize the query results
*/
export interface AbstractQueryModifiers {
limit?: AbstractQueryNodeLimit;
offset?: AbstractQueryNodeOffset;
sort?: AbstractQueryNodeSort[];
filter?: AbstractQueryFilterNode;
}
export * from './filters.js';
export * from './filters/index.js';
export * from './limit.js';
export * from './offset.js';
export * from './sort.js';
export * from './filters/index.js';

View File

@@ -1,6 +1,7 @@
import type { AbstractQuery } from './abstract-query/abstract-query.js';
import type { ReadableStream } from 'node:stream/web';
import type { AbstractQuery } from './abstract-query.js';
export abstract class DataDriver {
abstract query: (query: AbstractQuery) => Promise<ReadableStream>;

View File

@@ -1,2 +1,3 @@
export * from './abstract-query/abstract-query.js';
export * from './abstract-query.js';
export * from './abstract-query/index.js';
export * from './driver.js';