@directus/data-sql
Advanced tools
Comparing version 0.3.0 to 0.3.1
@@ -1,4 +0,4 @@ | ||
import { ExtractFn, ArrayFn, AbstractQueryNodeSortTargets, AbstractQuery } from '@directus/data'; | ||
import { ExtractFn, ArrayFn, AtLeastOneElement, AbstractQuery } from '@directus/data'; | ||
import { GeoJSONGeometry } from 'wellknown'; | ||
import { TransformStream } from 'node:stream/web'; | ||
import { ReadableStream } from 'node:stream/web'; | ||
@@ -39,14 +39,27 @@ /** | ||
} | ||
/** | ||
* Used to apply a function to a column. | ||
* Currently we support various EXTRACT functions to extract specific parts out of a data/time value. | ||
*/ | ||
interface AbstractSqlQuerySelectFnNode extends AbstractSqlQueryColumn { | ||
type: 'fn'; | ||
/** | ||
* A list of supported functions. Those are the same as the abstract query. | ||
*/ | ||
fn: ExtractFn | ArrayFn; | ||
arguments?: ValuesNode; | ||
as?: string; | ||
} | ||
/** | ||
* Used to select a specific column from a table. | ||
*/ | ||
interface AbstractSqlQuerySelectNode extends AbstractSqlQueryColumn { | ||
interface AbstractSqlQuerySelectPrimitiveNode extends AbstractSqlQueryColumn { | ||
type: 'primitive'; | ||
as?: string; | ||
/** | ||
* The final alias optionally provided by the user which will be returned within the response. | ||
*/ | ||
alias?: string; | ||
} | ||
type AbstractSqlQuerySelectNode = AbstractSqlQuerySelectPrimitiveNode | AbstractSqlQuerySelectFnNode; | ||
/** | ||
@@ -82,17 +95,2 @@ * Condition to filter rows where two columns of different tables are equal. | ||
/** | ||
* Used to apply a function to a column. | ||
* Currently we support various EXTRACT functions to extract specific parts out of a data/time value. | ||
*/ | ||
interface AbstractSqlQueryFnNode extends AbstractSqlQueryColumn { | ||
type: 'fn'; | ||
/** | ||
* A list of supported functions. Those are the same as the abstract query. | ||
*/ | ||
fn: ExtractFn | ArrayFn; | ||
arguments?: ValuesNode; | ||
as?: string; | ||
alias?: string; | ||
} | ||
/** | ||
* Filter rows where a numeric column is equal, greater than, less than, etc. other given number. | ||
@@ -102,3 +100,3 @@ */ | ||
type: 'condition-number'; | ||
target: AbstractSqlQuerySelectNode | AbstractSqlQueryFnNode; | ||
target: AbstractSqlQuerySelectNode; | ||
operation: 'eq' | 'lt' | 'lte' | 'gt' | 'gte'; | ||
@@ -135,3 +133,2 @@ compareTo: ValueNode; | ||
} | ||
type SqlConditionType = 'condition-string' | 'condition-number' | 'condition-geo' | 'condition-set' | 'condition-field'; | ||
@@ -145,3 +142,3 @@ /** | ||
negate: boolean; | ||
childNodes: (AbstractSqlQueryConditionNode | AbstractSqlQueryLogicalNode)[]; | ||
childNodes: AtLeastOneElement<AbstractSqlQueryConditionNode | AbstractSqlQueryLogicalNode>; | ||
} | ||
@@ -157,3 +154,2 @@ | ||
as: string; | ||
alias?: string; | ||
} | ||
@@ -163,8 +159,10 @@ | ||
type: 'order'; | ||
orderBy: AbstractQueryNodeSortTargets; | ||
orderBy: AbstractSqlQuerySelectNode; | ||
direction: 'ASC' | 'DESC'; | ||
} | ||
type AbstractSqlQueryWhereNode = AbstractSqlQueryConditionNode | AbstractSqlQueryLogicalNode; | ||
interface AbstractSqlClauses { | ||
select: (AbstractSqlQuerySelectNode | AbstractSqlQueryFnNode)[]; | ||
select: AbstractSqlQuerySelectNode[]; | ||
from: string; | ||
@@ -177,7 +175,2 @@ joins?: AbstractSqlQueryJoinNode[]; | ||
} | ||
type AbstractSqlQueryWhereNode = AbstractSqlQueryConditionNode | AbstractSqlQueryLogicalNode; | ||
type WhereUnion = { | ||
where: AbstractSqlQueryWhereNode; | ||
parameters: ParameterTypes[]; | ||
}; | ||
@@ -206,5 +199,7 @@ /** | ||
* const query: SqlStatement = { | ||
* select: [title], | ||
* from: 'articles', | ||
* limit: 0, // this is the index of the parameter | ||
* clauses: { | ||
* select: [title], | ||
* from: 'articles', | ||
* limit: 0, // this is the index of the parameter | ||
* }, | ||
* parameters: [25], | ||
@@ -217,4 +212,26 @@ * }; | ||
parameters: ParameterTypes[]; | ||
aliasMapping: Map<string, string[]>; | ||
} | ||
type SubQuery = (rootRow: Record<string, unknown>) => { | ||
rootQuery: AbstractSqlQuery; | ||
subQueries: SubQuery[]; | ||
aliasMapping: AliasMapping; | ||
}; | ||
type AliasMapping = ({ | ||
type: 'nested'; | ||
alias: string; | ||
children: AliasMapping; | ||
} | { | ||
type: 'root'; | ||
alias: string; | ||
column: string; | ||
} | { | ||
type: 'sub'; | ||
alias: string; | ||
index: number; | ||
})[]; | ||
interface ConverterResult { | ||
rootQuery: AbstractSqlQuery; | ||
subQueries: SubQuery[]; | ||
aliasMapping: AliasMapping; | ||
} | ||
@@ -236,21 +253,5 @@ /** | ||
*/ | ||
declare const convertQuery: (abstractQuery: AbstractQuery) => AbstractSqlQuery; | ||
declare const convertQuery: (abstractQuery: AbstractQuery) => ConverterResult; | ||
/** | ||
* Converts the receiving chunks from the database into a nested structure | ||
* based on the result from the database. | ||
*/ | ||
declare const getOrmTransformer: (paths: Map<string, string[]>) => TransformStream; | ||
/** | ||
* It takes the chunk from the stream and transforms the | ||
* flat result from the database (basically a two dimensional matrix) | ||
* into to proper nested javascript object. | ||
* | ||
* @param chunk one row of the database response | ||
* @param paths the lookup map from the aliases to the nested path | ||
* @returns an object which reflects the hierarchy from the initial query | ||
*/ | ||
declare function transformChunk(chunk: Record<string, any>, paths: Map<string, string[]>): Record<string, any>; | ||
/** | ||
* Appends a pseudo-random value to the end of a given identifier to make sure it's unique within the | ||
@@ -269,2 +270,57 @@ * context of the current query. The generated identifier is used as an alias to select columns and to join tables. | ||
/** | ||
* This logic handles o2m relational nodes which can be seen as default implementation/behavior for all SQL drivers. | ||
* | ||
* @param rootStream the stream of the root query | ||
* @param subQueries the sub query generators that generate queries to query relational data | ||
* @param aliasMapping the mapping that maps the result structure to root rows and sub query results | ||
* @param queryDatabase a function which is defined in the drivers which queries the database | ||
* @returns the final stream which contains the mapped root query and sub query results | ||
*/ | ||
declare function getMappedQueriesStream(rootStream: ReadableStream<Record<string, unknown>>, subQueries: SubQuery[], aliasMapping: AliasMapping, queryDatabase: (query: AbstractSqlQuery) => Promise<ReadableStream<Record<string, unknown>>>): ReadableStream<Record<string, unknown>>; | ||
/** | ||
* This unit takes care of transforming the response from the database into into a nested JSON object. | ||
* | ||
* @remarks | ||
* A SQL database returns the result as a two dimensional table, where the actual data are the rows and the columns specify the fields. | ||
* Such result is a flat Javascript object but we want to return a nested object which maps the realtionships form the database into a nested structure of an object. | ||
* | ||
* @example | ||
* Let's say we have the two collections articles and authors and we want to query all articles with all data about the author as well. | ||
* This would be the SQL query: | ||
* | ||
* ```sql | ||
* select * from articles join authors on authors.id = articles.author; | ||
* ``` | ||
* The following is a chunk from an example response from the database: | ||
* ```json | ||
* { | ||
* "id": 1, | ||
* "status": "published", | ||
* "author": 1, | ||
* "title": "some news", | ||
* "name": "jan" | ||
* }, | ||
* ``` | ||
* The first four rows were stored in the articles table, the last two in the authors table. | ||
* But what we want to return to the user is the following: | ||
* ```json | ||
* { | ||
* "id": 1, | ||
* "status": "published", | ||
* "title": "some news", | ||
* "author": { | ||
* "id": 1, | ||
* "name": "jan" | ||
* }, | ||
* }, | ||
* ``` | ||
* That's what this unit is for. | ||
* | ||
* @module | ||
*/ | ||
declare function mapResult(aliasMapping: AliasMapping, rootRow: Record<string, unknown>, subResult: Record<string, unknown>[][]): Record<string, unknown>; | ||
/** | ||
* @param operation | ||
@@ -276,2 +332,10 @@ * @param negate | ||
export { AbstractSqlClauses, AbstractSqlQuery, AbstractSqlQueryColumn, AbstractSqlQueryConditionNode, AbstractSqlQueryFnNode, AbstractSqlQueryJoinNode, AbstractSqlQueryLogicalNode, AbstractSqlQueryOrderNode, AbstractSqlQuerySelectNode, AbstractSqlQueryWhereNode, ParameterTypes, ParameterizedSqlStatement, SqlConditionFieldNode, SqlConditionGeoNode, SqlConditionNumberNode, SqlConditionSetNode, SqlConditionStringNode, SqlConditionType, ValueNode, ValuesNode, WhereUnion, convertNumericOperators, convertQuery, createUniqueAlias, getOrmTransformer, transformChunk }; | ||
/** | ||
* Receives all the chunks from a stream until it's empty. | ||
* | ||
* @param readableStream the stream to be consumed | ||
* @returns all the data from a stream | ||
*/ | ||
declare function readToEnd(readableStream: ReadableStream<Record<string, unknown>>): Promise<Record<string, unknown>[]>; | ||
export { type AbstractSqlClauses, type AbstractSqlQuery, type AbstractSqlQueryColumn, type AbstractSqlQueryConditionNode, type AbstractSqlQueryJoinNode, type AbstractSqlQueryLogicalNode, type AbstractSqlQueryOrderNode, type AbstractSqlQuerySelectFnNode, type AbstractSqlQuerySelectNode, type AbstractSqlQuerySelectPrimitiveNode, type AbstractSqlQueryWhereNode, type AliasMapping, type ConverterResult, type ParameterTypes, type ParameterizedSqlStatement, type SqlConditionFieldNode, type SqlConditionGeoNode, type SqlConditionNumberNode, type SqlConditionSetNode, type SqlConditionStringNode, type SubQuery, type ValueNode, type ValuesNode, convertNumericOperators, convertQuery, createUniqueAlias, getMappedQueriesStream, mapResult, readToEnd }; |
@@ -1,27 +0,35 @@ | ||
// src/query-converter/param-index-generator.ts | ||
function* parameterIndexGenerator() { | ||
let index = 0; | ||
while (true) { | ||
yield index++; | ||
} | ||
} | ||
// src/utils/create-unique-alias.ts | ||
import { randomBytes } from "crypto"; | ||
var createUniqueAlias = (identifier) => { | ||
const random = randomBytes(3).toString("hex"); | ||
return `${identifier}_${random}`; | ||
}; | ||
// src/query-converter/fields/create-primitive-select.ts | ||
var createPrimitiveSelect = (collection, abstractPrimitive, generatedAlias) => { | ||
const primitive = { | ||
type: "primitive", | ||
// src/query-converter/functions.ts | ||
function convertFn(collection, abstractFunction, idxGenerator, generatedAlias) { | ||
const fn = { | ||
type: "fn", | ||
fn: abstractFunction.fn, | ||
table: collection, | ||
column: abstractPrimitive.field, | ||
as: generatedAlias | ||
column: abstractFunction.field | ||
}; | ||
if (abstractPrimitive.alias) { | ||
primitive.alias = abstractPrimitive.alias; | ||
if (generatedAlias) { | ||
fn.as = generatedAlias; | ||
} | ||
return primitive; | ||
}; | ||
if (abstractFunction.args && abstractFunction.args?.length > 0) { | ||
fn.arguments = { | ||
type: "values", | ||
parameterIndexes: abstractFunction.args.map(() => idxGenerator.next().value) | ||
}; | ||
} | ||
return { | ||
fn, | ||
parameters: abstractFunction.args ?? [] | ||
}; | ||
} | ||
// src/query-converter/fields/create-join.ts | ||
var createJoin = (currentCollection, relationalField, externalCollectionAlias, fieldAlias) => { | ||
var createJoin = (currentCollection, relationalField, externalCollectionAlias) => { | ||
let on; | ||
if (relationalField.join.current.fields.length > 1) { | ||
if (relationalField.local.fields.length > 1) { | ||
on = { | ||
@@ -31,4 +39,4 @@ type: "logical", | ||
negate: false, | ||
childNodes: relationalField.join.current.fields.map((currentField, index) => { | ||
const externalField = relationalField.join.external.fields[index]; | ||
childNodes: relationalField.local.fields.map((currentField, index) => { | ||
const externalField = relationalField.foreign.fields[index]; | ||
if (!externalField) { | ||
@@ -43,5 +51,5 @@ throw new Error(`Missing related foreign key join column for current context column "${currentField}"`); | ||
currentCollection, | ||
relationalField.join.current.fields[0], | ||
relationalField.local.fields[0], | ||
externalCollectionAlias, | ||
relationalField.join.external.fields[0] | ||
relationalField.foreign.fields[0] | ||
); | ||
@@ -51,9 +59,6 @@ } | ||
type: "join", | ||
table: relationalField.join.external.collection, | ||
table: relationalField.foreign.collection, | ||
as: externalCollectionAlias, | ||
on | ||
}; | ||
if (fieldAlias) { | ||
result.alias = fieldAlias; | ||
} | ||
return result; | ||
@@ -82,130 +87,54 @@ }; | ||
// src/query-converter/functions.ts | ||
function convertFn(collection, abstractFunction, idxGenerator, generatedAlias) { | ||
const fn = { | ||
type: "fn", | ||
fn: abstractFunction.fn, | ||
table: collection, | ||
column: abstractFunction.field | ||
}; | ||
if (abstractFunction.alias) { | ||
fn.alias = abstractFunction.alias; | ||
} | ||
if (generatedAlias) { | ||
fn.as = generatedAlias; | ||
} | ||
if (abstractFunction.args && abstractFunction.args?.length > 0) { | ||
fn.arguments = { | ||
type: "values", | ||
parameterIndexes: abstractFunction.args.map(() => idxGenerator.next().value) | ||
// src/query-converter/modifiers/target.ts | ||
function convertTarget(target, collection, idxGenerator) { | ||
if (target.type === "primitive") { | ||
return { | ||
value: { | ||
type: "primitive", | ||
table: collection, | ||
column: target.field | ||
}, | ||
joins: [] | ||
}; | ||
} else if (target.type === "fn") { | ||
const convertedFn = convertFn(collection, target, idxGenerator); | ||
return { | ||
value: convertedFn.fn, | ||
joins: [] | ||
}; | ||
} else { | ||
const { value, joins } = convertNestedOneTarget(collection, target, idxGenerator); | ||
return { | ||
value, | ||
joins | ||
}; | ||
} | ||
return { | ||
fn, | ||
parameters: abstractFunction.args ?? [] | ||
}; | ||
} | ||
// src/orm/create-unique-alias.ts | ||
import { randomBytes } from "crypto"; | ||
var createUniqueAlias = (identifier) => { | ||
const random = randomBytes(3).toString("hex"); | ||
return `${identifier}_${random}`; | ||
}; | ||
// src/query-converter/fields/fields.ts | ||
var convertFieldNodes = (collection, abstractFields, idxGenerator, currentPath = []) => { | ||
const select = []; | ||
const joins = []; | ||
const parameters = []; | ||
const aliasRelationalMapping = /* @__PURE__ */ new Map(); | ||
for (const abstractField of abstractFields) { | ||
if (abstractField.type === "primitive") { | ||
const generatedAlias = createUniqueAlias(abstractField.field); | ||
aliasRelationalMapping.set(generatedAlias, [...currentPath, abstractField.alias ?? abstractField.field]); | ||
const selectNode = createPrimitiveSelect(collection, abstractField, generatedAlias); | ||
select.push(selectNode); | ||
continue; | ||
} | ||
if (abstractField.type === "nested-one") { | ||
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.meta.join.external.collection | ||
]); | ||
nestedOutput.aliasMapping.forEach((value, key) => aliasRelationalMapping.set(key, value)); | ||
joins.push(sqlJoinNode); | ||
select.push(...nestedOutput.clauses.select); | ||
} | ||
continue; | ||
} | ||
if (abstractField.type === "fn") { | ||
const fnField = abstractField; | ||
const generatedAlias = createUniqueAlias(`${fnField.fn.fn}_${fnField.field}`); | ||
aliasRelationalMapping.set(generatedAlias, [...currentPath, abstractField.alias ?? abstractField.field]); | ||
const fn = convertFn(collection, fnField, idxGenerator, generatedAlias); | ||
select.push(fn.fn); | ||
parameters.push(...fn.parameters); | ||
continue; | ||
} | ||
} | ||
return { clauses: { select, joins }, parameters, aliasMapping: aliasRelationalMapping }; | ||
}; | ||
// src/query-converter/modifiers/filter/logical.ts | ||
function convertLogical(children, operator, negate) { | ||
const childNodes = children.map((child) => child.where); | ||
const parameters = children.flatMap((child) => child.parameters); | ||
function convertNestedOneTarget(currentCollection, nestedTarget, idxGenerator) { | ||
const externalCollectionAlias = createUniqueAlias(nestedTarget.nesting.foreign.collection); | ||
const join = createJoin(currentCollection, nestedTarget.nesting, externalCollectionAlias); | ||
const { value, joins } = convertTarget(nestedTarget.field, externalCollectionAlias, idxGenerator); | ||
return { | ||
where: { | ||
type: "logical", | ||
negate, | ||
operator, | ||
childNodes | ||
}, | ||
parameters | ||
value, | ||
joins: [join, ...joins] | ||
}; | ||
} | ||
// src/query-converter/modifiers/filter/conditions/utils.ts | ||
function convertPrimitive(collection, primitiveNode) { | ||
return { | ||
type: "primitive", | ||
table: collection, | ||
column: primitiveNode.field | ||
}; | ||
} | ||
function convertTarget(condition, collection, generator) { | ||
let target; | ||
const parameters = []; | ||
if (condition.target.type === "primitive") { | ||
target = { | ||
type: "primitive", | ||
table: collection, | ||
column: condition.target.field | ||
}; | ||
} else if (condition.target.type === "fn") { | ||
const convertedFn = convertFn(collection, condition.target, generator); | ||
target = convertedFn.fn; | ||
parameters.push(...convertedFn.parameters); | ||
} else { | ||
throw new Error("The related field types are not yet supported."); | ||
} | ||
return target; | ||
} | ||
// src/query-converter/modifiers/filter/conditions/field.ts | ||
function convertFieldCondition(node, collection, negate) { | ||
function convertFieldCondition(node, collection, generator, negate) { | ||
const { value: value1, joins: joins1 } = convertTarget(node.target, collection, generator); | ||
const { value: value2, joins: joins2 } = convertTarget(node.compareTo, collection, generator); | ||
return { | ||
where: { | ||
type: "condition", | ||
negate, | ||
condition: { | ||
type: "condition-field", | ||
operation: node.operation, | ||
target: convertPrimitive(collection, node.target), | ||
compareTo: convertPrimitive(node.compareTo.collection, node.compareTo) | ||
} | ||
clauses: { | ||
where: { | ||
type: "condition", | ||
negate, | ||
condition: { | ||
type: "condition-field", | ||
operation: node.operation, | ||
target: value1, | ||
compareTo: value2 | ||
} | ||
}, | ||
joins: [...joins1, ...joins2] | ||
}, | ||
@@ -218,15 +147,19 @@ parameters: [] | ||
function convertGeoCondition(node, collection, generator, negate) { | ||
const { value, joins } = convertTarget(node.target, collection, generator); | ||
return { | ||
where: { | ||
type: "condition", | ||
negate, | ||
condition: { | ||
type: "condition-geo", | ||
operation: node.operation, | ||
target: convertPrimitive(collection, node.target), | ||
compareTo: { | ||
type: "value", | ||
parameterIndex: generator.next().value | ||
clauses: { | ||
where: { | ||
type: "condition", | ||
negate, | ||
condition: { | ||
type: "condition-geo", | ||
operation: node.operation, | ||
target: value, | ||
compareTo: { | ||
type: "value", | ||
parameterIndex: generator.next().value | ||
} | ||
} | ||
} | ||
}, | ||
joins | ||
}, | ||
@@ -239,15 +172,19 @@ parameters: [node.compareTo] | ||
function convertStringNode(node, collection, generator, negate) { | ||
const { value, joins } = convertTarget(node.target, collection, generator); | ||
return { | ||
where: { | ||
type: "condition", | ||
negate, | ||
condition: { | ||
type: node.type, | ||
operation: node.operation, | ||
target: convertPrimitive(collection, node.target), | ||
compareTo: { | ||
type: "value", | ||
parameterIndex: generator.next().value | ||
clauses: { | ||
where: { | ||
type: "condition", | ||
negate, | ||
condition: { | ||
type: node.type, | ||
operation: node.operation, | ||
target: value, | ||
compareTo: { | ||
type: "value", | ||
parameterIndex: generator.next().value | ||
} | ||
} | ||
} | ||
}, | ||
joins | ||
}, | ||
@@ -260,15 +197,19 @@ parameters: [node.compareTo] | ||
function convertNumberNode(node, collection, generator, negate) { | ||
const { value, joins } = convertTarget(node.target, collection, generator); | ||
return { | ||
where: { | ||
type: "condition", | ||
negate, | ||
condition: { | ||
type: node.type, | ||
operation: node.operation, | ||
target: convertTarget(node, collection, generator), | ||
compareTo: { | ||
type: "value", | ||
parameterIndex: generator.next().value | ||
clauses: { | ||
where: { | ||
type: "condition", | ||
negate, | ||
condition: { | ||
type: node.type, | ||
operation: node.operation, | ||
target: value, | ||
compareTo: { | ||
type: "value", | ||
parameterIndex: generator.next().value | ||
} | ||
} | ||
} | ||
}, | ||
joins | ||
}, | ||
@@ -281,15 +222,19 @@ parameters: [node.compareTo] | ||
function convertSetCondition(node, collection, generator, negate) { | ||
const { value, joins } = convertTarget(node.target, collection, generator); | ||
return { | ||
where: { | ||
type: "condition", | ||
negate, | ||
condition: { | ||
type: "condition-set", | ||
operation: node.operation, | ||
target: convertPrimitive(collection, node.target), | ||
compareTo: { | ||
type: "values", | ||
parameterIndexes: Array.from(node.compareTo).map(() => generator.next().value) | ||
clauses: { | ||
where: { | ||
type: "condition", | ||
negate, | ||
condition: { | ||
type: "condition-set", | ||
operation: node.operation, | ||
target: value, | ||
compareTo: { | ||
type: "values", | ||
parameterIndexes: Array.from(node.compareTo).map(() => generator.next().value) | ||
} | ||
} | ||
} | ||
}, | ||
joins | ||
}, | ||
@@ -313,6 +258,27 @@ parameters: [...node.compareTo] | ||
case "condition-field": | ||
return convertFieldCondition(condition.condition, collection, negate); | ||
return convertFieldCondition(condition.condition, collection, generator, negate); | ||
} | ||
} | ||
// src/query-converter/modifiers/filter/logical.ts | ||
function convertLogical(children, operator, negate) { | ||
const childWhereClauses = children.map( | ||
(child) => child.clauses.where | ||
); | ||
const parameters = children.flatMap((child) => child.parameters); | ||
const joins = children.flatMap((child) => child.clauses.joins); | ||
return { | ||
clauses: { | ||
where: { | ||
type: "logical", | ||
negate, | ||
operator, | ||
childNodes: childWhereClauses | ||
}, | ||
joins | ||
}, | ||
parameters | ||
}; | ||
} | ||
// src/query-converter/modifiers/filter/filter.ts | ||
@@ -325,3 +291,5 @@ var convertFilter = (filter, collection, generator, negate = false) => { | ||
} else if (filter.type === "logical") { | ||
const children = filter.childNodes.map((childNode) => convertFilter(childNode, collection, generator, false)); | ||
const children = filter.childNodes.map( | ||
(childNode) => convertFilter(childNode, collection, generator, false) | ||
); | ||
return convertLogical(children, filter.operator, negate); | ||
@@ -334,10 +302,21 @@ } else { | ||
// src/query-converter/modifiers/sort.ts | ||
var convertSort = (abstractSorts) => { | ||
return abstractSorts.map((abstractSort) => { | ||
return { | ||
var convertSort = (abstractSorts, collection, idxGenerator) => { | ||
const result = { | ||
clauses: { | ||
joins: [], | ||
order: [] | ||
} | ||
}; | ||
abstractSorts.forEach((abstractSort) => { | ||
const targetConversionResult = convertTarget(abstractSort.target, collection, idxGenerator); | ||
const orderBy = { | ||
type: "order", | ||
orderBy: abstractSort.target, | ||
orderBy: targetConversionResult.value, | ||
direction: abstractSort.direction === "descending" ? "DESC" : "ASC" | ||
}; | ||
result.clauses.order.push(orderBy); | ||
result.clauses.joins.push(...targetConversionResult.joins); | ||
return result; | ||
}); | ||
return result; | ||
}; | ||
@@ -351,17 +330,28 @@ | ||
}; | ||
if (modifiers?.filter) { | ||
if (modifiers.filter) { | ||
const convertedFilter = convertFilter(modifiers.filter, collection, idxGenerator); | ||
result.clauses.where = convertedFilter.where; | ||
result.clauses.where = convertedFilter.clauses.where; | ||
if (convertedFilter.clauses.joins.length > 0) { | ||
result.clauses.joins = convertedFilter.clauses.joins; | ||
} | ||
result.parameters.push(...convertedFilter.parameters); | ||
} | ||
if (modifiers?.limit) { | ||
if (modifiers.limit) { | ||
result.clauses.limit = { type: "value", parameterIndex: idxGenerator.next().value }; | ||
result.parameters.push(modifiers.limit.value); | ||
} | ||
if (modifiers?.offset) { | ||
if (modifiers.offset) { | ||
result.clauses.offset = { type: "value", parameterIndex: idxGenerator.next().value }; | ||
result.parameters.push(modifiers.offset.value); | ||
} | ||
if (modifiers?.sort) { | ||
result.clauses.order = convertSort(modifiers.sort); | ||
if (modifiers.sort) { | ||
const sortConversionResult = convertSort(modifiers.sort, collection, idxGenerator); | ||
result.clauses.order = sortConversionResult.clauses.order; | ||
if (sortConversionResult.clauses.joins.length > 0) { | ||
if (result.clauses.joins) { | ||
result.clauses.joins.push(...sortConversionResult.clauses.joins); | ||
} else { | ||
result.clauses.joins = sortConversionResult.clauses.joins; | ||
} | ||
} | ||
} | ||
@@ -371,2 +361,150 @@ return result; | ||
// src/query-converter/param-index-generator.ts | ||
function* parameterIndexGenerator() { | ||
let index = 0; | ||
while (true) { | ||
yield index++; | ||
} | ||
} | ||
// src/query-converter/fields/create-primitive-select.ts | ||
var createPrimitiveSelect = (collection, field, generatedAlias) => { | ||
const primitive = { | ||
type: "primitive", | ||
table: collection, | ||
column: field, | ||
as: generatedAlias | ||
}; | ||
return primitive; | ||
}; | ||
// src/query-converter/fields/create-nested-manys.ts | ||
function getNestedMany(collection, field) { | ||
if (field.nesting.type !== "relational-many") | ||
throw new Error("Nested o2a not yet implemented!"); | ||
const index = parameterIndexGenerator(); | ||
const nestedFieldNodes = convertFieldNodes(field.nesting.foreign.collection, field.fields, index); | ||
const nestedModifiers = convertModifiers(field.modifiers, field.nesting.foreign.collection, index); | ||
const joins = [...nestedFieldNodes.clauses.joins, ...nestedModifiers.clauses.joins ?? []]; | ||
const parameters = [...nestedFieldNodes.parameters, ...nestedModifiers.parameters]; | ||
const clauses = { | ||
select: nestedFieldNodes.clauses.select, | ||
from: field.nesting.foreign.collection, | ||
...nestedModifiers.clauses, | ||
joins, | ||
where: nestedModifiers.clauses.where ? { | ||
type: "logical", | ||
operator: "and", | ||
negate: false, | ||
childNodes: [nestedModifiers.clauses.where, getRelationConditions(field.nesting, index)] | ||
} : getRelationConditions(field.nesting, index) | ||
}; | ||
const generatedAliases = field.nesting.local.fields.map((field2) => [field2, createUniqueAlias(field2)]); | ||
const generatedAliasMap = Object.fromEntries(generatedAliases); | ||
const select = generatedAliases.map(([field2, alias]) => createPrimitiveSelect(collection, field2, alias)); | ||
return { | ||
subQuery: (rootRow) => ({ | ||
rootQuery: { | ||
clauses, | ||
parameters: [ | ||
...parameters, | ||
...field.nesting.local.fields.map((field2) => rootRow[generatedAliasMap[field2]]) | ||
] | ||
}, | ||
subQueries: nestedFieldNodes.subQueries, | ||
aliasMapping: nestedFieldNodes.aliasMapping | ||
}), | ||
select | ||
}; | ||
} | ||
function getRelationConditions(fieldNesting, idxGenerator) { | ||
const table = fieldNesting.foreign.collection; | ||
if (fieldNesting.foreign.fields.length > 1) { | ||
return { | ||
type: "logical", | ||
operator: "and", | ||
negate: false, | ||
childNodes: fieldNesting.foreign.fields.map( | ||
(field) => getRelationCondition(table, field, idxGenerator) | ||
) | ||
}; | ||
} else { | ||
return getRelationCondition(table, fieldNesting.foreign.fields[0], idxGenerator); | ||
} | ||
} | ||
function getRelationCondition(table, column, idxGenerator) { | ||
return { | ||
type: "condition", | ||
condition: { | ||
type: "condition-string", | ||
// could also be a condition-number, but it doesn't matter because both support 'eq' | ||
operation: "eq", | ||
target: { | ||
type: "primitive", | ||
table, | ||
column | ||
}, | ||
compareTo: { | ||
type: "value", | ||
parameterIndex: idxGenerator.next().value | ||
} | ||
}, | ||
negate: false | ||
}; | ||
} | ||
// src/query-converter/fields/fields.ts | ||
var convertFieldNodes = (collection, abstractFields, idxGenerator) => { | ||
const select = []; | ||
const joins = []; | ||
const parameters = []; | ||
const aliasMapping = []; | ||
const subQueries = []; | ||
for (const abstractField of abstractFields) { | ||
if (abstractField.type === "primitive") { | ||
const generatedAlias = createUniqueAlias(abstractField.field); | ||
aliasMapping.push({ type: "root", alias: abstractField.alias, column: generatedAlias }); | ||
const selectNode = createPrimitiveSelect(collection, abstractField.field, generatedAlias); | ||
select.push(selectNode); | ||
continue; | ||
} | ||
if (abstractField.type === "nested-single-one") { | ||
if (abstractField.nesting.type === "relational-many") { | ||
const externalCollectionAlias = createUniqueAlias(abstractField.nesting.foreign.collection); | ||
const sqlJoinNode = createJoin(collection, abstractField.nesting, externalCollectionAlias); | ||
const nestedOutput = convertFieldNodes(externalCollectionAlias, abstractField.fields, idxGenerator); | ||
aliasMapping.push({ type: "nested", alias: abstractField.alias, children: nestedOutput.aliasMapping }); | ||
joins.push(sqlJoinNode); | ||
select.push(...nestedOutput.clauses.select); | ||
} | ||
continue; | ||
} | ||
if (abstractField.type === "nested-union-one") { | ||
continue; | ||
} | ||
if (abstractField.type === "nested-single-many") { | ||
const nestedManyResult = getNestedMany(collection, abstractField); | ||
aliasMapping.push({ type: "sub", alias: abstractField.alias, index: subQueries.length }); | ||
subQueries.push(nestedManyResult.subQuery); | ||
select.push(...nestedManyResult.select); | ||
continue; | ||
} | ||
if (abstractField.type === "fn") { | ||
const fnField = abstractField; | ||
const generatedAlias = createUniqueAlias(`${fnField.fn.fn}_${fnField.field}`); | ||
aliasMapping.push({ type: "root", alias: abstractField.alias, column: generatedAlias }); | ||
const fn = convertFn(collection, fnField, idxGenerator, generatedAlias); | ||
select.push(fn.fn); | ||
parameters.push(...fn.parameters); | ||
continue; | ||
} | ||
} | ||
return { | ||
clauses: { select, joins }, | ||
subQueries, | ||
parameters, | ||
aliasMapping | ||
}; | ||
}; | ||
// src/query-converter/converter.ts | ||
@@ -376,37 +514,46 @@ var convertQuery = (abstractQuery) => { | ||
const parameters = []; | ||
const convertedFieldNodes = convertFieldNodes(abstractQuery.collection, abstractQuery.fields, idGen); | ||
let clauses = { ...convertedFieldNodes.clauses, from: abstractQuery.collection }; | ||
parameters.push(...convertedFieldNodes.parameters); | ||
const convertedModifiers = convertModifiers(abstractQuery.modifiers, abstractQuery.collection, idGen); | ||
clauses = Object.assign(clauses, convertedModifiers.clauses); | ||
parameters.push(...convertedModifiers.parameters); | ||
const subQueries = []; | ||
let clauses; | ||
let aliasMapping; | ||
try { | ||
const convertedFieldNodes = convertFieldNodes(abstractQuery.collection, abstractQuery.fields, idGen); | ||
clauses = { ...convertedFieldNodes.clauses, from: abstractQuery.collection }; | ||
parameters.push(...convertedFieldNodes.parameters); | ||
aliasMapping = convertedFieldNodes.aliasMapping; | ||
subQueries.push(...convertedFieldNodes.subQueries); | ||
} catch (error) { | ||
throw new Error(`Failed to convert query fields: ${error.message}`); | ||
} | ||
try { | ||
const convertedModifiers = convertModifiers(abstractQuery.modifiers, abstractQuery.collection, idGen); | ||
const joins = [...clauses.joins ?? [], ...convertedModifiers.clauses.joins ?? []]; | ||
clauses = { ...clauses, ...convertedModifiers.clauses, joins }; | ||
parameters.push(...convertedModifiers.parameters); | ||
} catch (error) { | ||
throw new Error(`Failed to convert query modifiers: ${error.message}`); | ||
} | ||
return { | ||
clauses, | ||
parameters, | ||
aliasMapping: convertedFieldNodes.aliasMapping | ||
rootQuery: { | ||
clauses, | ||
parameters | ||
}, | ||
subQueries, | ||
aliasMapping | ||
}; | ||
}; | ||
// src/orm/expand.ts | ||
import { set } from "lodash-es"; | ||
import { TransformStream } from "stream/web"; | ||
var getOrmTransformer = (paths) => { | ||
return new TransformStream({ | ||
transform(chunk, controller) { | ||
if (chunk?.constructor !== Object) { | ||
throw new Error(`Can't expand a non-object chunk`); | ||
} | ||
const outputChunk = transformChunk(chunk, paths); | ||
controller.enqueue(outputChunk); | ||
} | ||
}); | ||
}; | ||
function transformChunk(chunk, paths) { | ||
// src/utils/get-mapped-queries-stream.ts | ||
import { ReadableStream } from "stream/web"; | ||
// src/utils/map-result.ts | ||
function mapResult(aliasMapping, rootRow, subResult) { | ||
const result = {}; | ||
for (const [key, value] of Object.entries(chunk)) { | ||
const path = paths.get(key); | ||
if (!path) { | ||
throw new Error(`No path available for dot-notated key ${key}`); | ||
for (const aliasObject of aliasMapping) { | ||
if (aliasObject.type === "root") { | ||
result[aliasObject.alias] = rootRow[aliasObject.column]; | ||
} else if (aliasObject.type === "sub") { | ||
result[aliasObject.alias] = subResult[aliasObject.index]; | ||
} else { | ||
result[aliasObject.alias] = mapResult(aliasObject.children, rootRow, subResult); | ||
} | ||
set(result, path, value); | ||
} | ||
@@ -416,2 +563,37 @@ return result; | ||
// src/utils/stream-consumer.ts | ||
async function readToEnd(readableStream) { | ||
const actualResult = []; | ||
for await (const chunk of readableStream) { | ||
actualResult.push(chunk); | ||
} | ||
return actualResult; | ||
} | ||
// src/utils/get-mapped-queries-stream.ts | ||
function getMappedQueriesStream(rootStream, subQueries, aliasMapping, queryDatabase) { | ||
return new ReadableStream({ | ||
async start(controller) { | ||
for await (const rootRow of rootStream) { | ||
const subResult = await Promise.all( | ||
subQueries.map(async (subQuery) => { | ||
const generatedSubQuery = subQuery(rootRow); | ||
const subStream = await queryDatabase(generatedSubQuery.rootQuery); | ||
const mappedQueriesStream = getMappedQueriesStream( | ||
subStream, | ||
generatedSubQuery.subQueries, | ||
generatedSubQuery.aliasMapping, | ||
queryDatabase | ||
); | ||
return readToEnd(mappedQueriesStream); | ||
}) | ||
); | ||
const result = mapResult(aliasMapping, rootRow, subResult); | ||
controller.enqueue(result); | ||
} | ||
controller.close(); | ||
} | ||
}); | ||
} | ||
// src/utils/numeric-operator-conversion.ts | ||
@@ -445,4 +627,5 @@ function convertNumericOperators(operation, negate) { | ||
createUniqueAlias, | ||
getOrmTransformer, | ||
transformChunk | ||
getMappedQueriesStream, | ||
mapResult, | ||
readToEnd | ||
}; |
{ | ||
"name": "@directus/data-sql", | ||
"version": "0.3.0", | ||
"version": "0.3.1", | ||
"type": "module", | ||
@@ -24,14 +24,13 @@ "sideEffects": false, | ||
"devDependencies": { | ||
"@types/lodash-es": "4.17.7", | ||
"@types/node": "18.16.12", | ||
"@types/wellknown": "0.5.4", | ||
"@vitest/coverage-c8": "0.31.1", | ||
"dependency-cruiser": "13.1.4", | ||
"tsup": "7.2.0", | ||
"typescript": "5.2.2", | ||
"vitest": "0.31.1", | ||
"@directus/data": "0.3.0", | ||
"@types/lodash-es": "4.17.12", | ||
"@types/node": "18.19.3", | ||
"@types/wellknown": "0.5.8", | ||
"@vitest/coverage-v8": "1.1.0", | ||
"dependency-cruiser": "15.5.0", | ||
"tsup": "8.0.1", | ||
"typescript": "5.3.3", | ||
"vitest": "1.1.0", | ||
"@directus/random": "0.2.4", | ||
"@directus/tsconfig": "1.0.1", | ||
"@directus/random": "0.2.3", | ||
"@directus/types": "11.0.0" | ||
"@directus/data": "0.3.1" | ||
}, | ||
@@ -38,0 +37,0 @@ "dependencies": { |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
38050
11
896
0