@directus/data-sql
Advanced tools
Comparing version 0.2.1 to 0.3.0
@@ -1,86 +0,264 @@ | ||
import { AbstractQueryNodeSortTargets, AbstractQuery } from '@directus/data'; | ||
import { ExtractFn, ArrayFn, AbstractQueryNodeSortTargets, AbstractQuery } from '@directus/data'; | ||
import { GeoJSONGeometry } from 'wellknown'; | ||
import { TransformStream } from 'node:stream/web'; | ||
interface SqlStatementColumn { | ||
type: 'primitive'; | ||
/** | ||
* Used pass a single value. | ||
*/ | ||
interface ValueNode { | ||
type: 'value'; | ||
parameterIndex: number; | ||
} | ||
/** | ||
* Used pass an arbitrary amount of values. | ||
*/ | ||
interface ValuesNode { | ||
type: 'values'; | ||
parameterIndexes: number[]; | ||
} | ||
/** | ||
* An actual vendor specific SQL statement with its parameters. | ||
* @example | ||
* ``` | ||
* { | ||
* statement: 'SELECT * FROM "articles" WHERE "articles"."id" = $1;', | ||
* values: [99], | ||
* } | ||
* ``` | ||
*/ | ||
interface ParameterizedSqlStatement { | ||
statement: string; | ||
parameters: ParameterTypes[]; | ||
} | ||
type ParameterTypes = string | boolean | number | GeoJSONGeometry; | ||
interface AbstractSqlQueryColumn { | ||
table: string; | ||
column: string; | ||
} | ||
interface SqlStatementSelectColumn extends SqlStatementColumn { | ||
/** | ||
* Used to select a specific column from a table. | ||
*/ | ||
interface AbstractSqlQuerySelectNode extends AbstractSqlQueryColumn { | ||
type: 'primitive'; | ||
as?: string; | ||
/** | ||
* The final alias optionally provided by the user which will be returned within the response. | ||
*/ | ||
alias?: string; | ||
} | ||
/** | ||
* Used for parameterized queries. | ||
* Condition to filter rows where two columns of different tables are equal. | ||
* Mainly used for JOINs. | ||
*/ | ||
type ParameterIndex = { | ||
/** Indicates where the actual value is stored in the parameter array */ | ||
parameterIndex: number; | ||
}; | ||
interface SqlConditionFieldNode { | ||
type: 'condition-field'; | ||
operation: 'eq'; | ||
target: AbstractSqlQuerySelectNode; | ||
compareTo: AbstractSqlQuerySelectNode; | ||
} | ||
/** | ||
* This is an abstract SQL query which can be passen to all SQL drivers. | ||
* | ||
* @example | ||
* ```ts | ||
* const query: SqlStatement = { | ||
* select: [id], | ||
* from: 'articles', | ||
* limit: 0, | ||
* parameters: [25], | ||
* }; | ||
* ``` | ||
* Used to retrieve a set of data, where the column in question stores a geographic value which intersects with another given geographic value. | ||
* Here, the two types `condition-geo` and `condition-geo-bbox` from @directus/data are combined into one type, | ||
* because the compare value is the same for both types - it's the reference to the actual value stored in the list of parameters. | ||
* That also why the operations got merged together. | ||
*/ | ||
interface AbstractSqlQuery { | ||
select: SqlStatementSelectColumn[]; | ||
from: string; | ||
limit?: ParameterIndex; | ||
offset?: ParameterIndex; | ||
order?: AbstractSqlQueryOrderNode[]; | ||
where?: AbstractSqlQueryWhereConditionNode | AbstractSqlQueryWhereLogicalNode; | ||
intersect?: AbstractSqlQuery; | ||
parameters: (string | boolean | number)[]; | ||
interface SqlConditionGeoNode { | ||
type: 'condition-geo'; | ||
target: AbstractSqlQuerySelectNode; | ||
/** | ||
* The operation to apply. Get only those rows where the targeting column | ||
* - `intersects`: intersects with the given geo value | ||
* - `intersects_bbox`: intersects with a given bounding box | ||
*/ | ||
operation: 'intersects' | 'intersects_bbox'; | ||
compareTo: ValueNode; | ||
} | ||
type AbstractSqlQueryOrderNode = { | ||
orderBy: AbstractQueryNodeSortTargets; | ||
direction: 'ASC' | 'DESC'; | ||
}; | ||
/** | ||
* An abstract WHERE clause. | ||
* 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 AbstractSqlQueryWhereConditionNode { | ||
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. | ||
*/ | ||
interface SqlConditionNumberNode { | ||
type: 'condition-number'; | ||
target: AbstractSqlQuerySelectNode | AbstractSqlQueryFnNode; | ||
operation: 'eq' | 'lt' | 'lte' | 'gt' | 'gte'; | ||
compareTo: ValueNode; | ||
} | ||
interface SqlConditionSetNode { | ||
type: 'condition-set'; | ||
operation: 'in'; | ||
target: AbstractSqlQuerySelectNode; | ||
compareTo: ValuesNode; | ||
} | ||
/** | ||
* Condition to filter rows where a string column value contains, starts with, ends with, or is equal to another given string. | ||
*/ | ||
interface SqlConditionStringNode { | ||
type: 'condition-string'; | ||
target: AbstractSqlQuerySelectNode; | ||
operation: 'contains' | 'starts_with' | 'ends_with' | 'eq'; | ||
compareTo: ValueNode; | ||
} | ||
/** | ||
* Condition to filter rows. | ||
* Various condition types are supported, each depending on a specific datatype. | ||
* The condition can also be negated on this level. | ||
*/ | ||
interface AbstractSqlQueryConditionNode { | ||
type: 'condition'; | ||
target: SqlStatementColumn; | ||
operation: 'eq' | 'lt' | 'lte' | 'gt' | 'gte' | 'in' | 'contains' | 'starts_with' | 'ends_with' | 'intersects'; | ||
condition: SqlConditionStringNode | SqlConditionNumberNode | SqlConditionGeoNode | SqlConditionSetNode | SqlConditionFieldNode; | ||
negate: boolean; | ||
compareTo: CompareValueNode; | ||
} | ||
interface AbstractSqlQueryWhereLogicalNode { | ||
type SqlConditionType = 'condition-string' | 'condition-number' | 'condition-geo' | 'condition-set' | 'condition-field'; | ||
/** | ||
* A wrapper to add multiple conditions at once. | ||
*/ | ||
interface AbstractSqlQueryLogicalNode { | ||
type: 'logical'; | ||
operator: 'and' | 'or'; | ||
negate: boolean; | ||
childNodes: (AbstractSqlQueryWhereConditionNode | AbstractSqlQueryWhereLogicalNode)[]; | ||
childNodes: (AbstractSqlQueryConditionNode | AbstractSqlQueryLogicalNode)[]; | ||
} | ||
interface CompareValueNode { | ||
type: 'value'; | ||
parameterIndexes: number[]; | ||
/** | ||
* Used to join another table, regardless of the type of relation. | ||
*/ | ||
interface AbstractSqlQueryJoinNode { | ||
type: 'join'; | ||
table: string; | ||
on: AbstractSqlQueryConditionNode | AbstractSqlQueryLogicalNode; | ||
as: string; | ||
alias?: string; | ||
} | ||
interface AbstractSqlQueryOrderNode { | ||
type: 'order'; | ||
orderBy: AbstractQueryNodeSortTargets; | ||
direction: 'ASC' | 'DESC'; | ||
} | ||
interface AbstractSqlClauses { | ||
select: (AbstractSqlQuerySelectNode | AbstractSqlQueryFnNode)[]; | ||
from: string; | ||
joins?: AbstractSqlQueryJoinNode[]; | ||
where?: AbstractSqlQueryWhereNode; | ||
limit?: ValueNode; | ||
offset?: ValueNode; | ||
order?: AbstractSqlQueryOrderNode[]; | ||
} | ||
type AbstractSqlQueryWhereNode = AbstractSqlQueryConditionNode | AbstractSqlQueryLogicalNode; | ||
type WhereUnion = { | ||
where: AbstractSqlQueryWhereNode; | ||
parameters: ParameterTypes[]; | ||
}; | ||
/** | ||
* An actual vendor specific SQL statement with its parameters. | ||
* A set of types which form the abstract SQL query. | ||
* It's still neutral to concrete SQL dialects and databases but provides to SQL drivers with a query type that they can more easy work with. | ||
* | ||
* How the abstract SQL query types differ from the abstract query. | ||
* - In the abstract query the user input values are put directly within the query directly. | ||
* The abstract SQL however stores the user input values in a list of parameters, so that the SQL driver always perform parameterized queries. | ||
* That way we prevent SQL injection. | ||
* Moving the user input values into a list of parameters and replace the input value with the index of the value from the list, is a big part of the converter. | ||
* - Instead of a wrapper for negation, here the negation is a property on the type. | ||
* So the abstract SQL does not have a node of type 'negate' but instead the nodes have a property called 'negate'. | ||
* | ||
* @module | ||
*/ | ||
/** | ||
* This is an abstract SQL query which can be passed to all SQL drivers. | ||
* | ||
* @example | ||
* The following query gets the title of all articles and limits the result to 25 rows. | ||
* ```ts | ||
* const query: SqlStatement = { | ||
* select: [title], | ||
* from: 'articles', | ||
* limit: 0, // this is the index of the parameter | ||
* parameters: [25], | ||
* }; | ||
* ``` | ||
* { | ||
* statement: 'SELECT * FROM "articles" WHERE "articles"."id" = $1;', | ||
* values: [99], | ||
* } | ||
* ``` | ||
*/ | ||
interface ParameterizedSQLStatement { | ||
statement: string; | ||
parameters: (string | number | boolean)[]; | ||
interface AbstractSqlQuery { | ||
clauses: AbstractSqlClauses; | ||
parameters: ParameterTypes[]; | ||
aliasMapping: Map<string, string[]>; | ||
} | ||
/** | ||
* Converts an abstract query to the abstract SQL query ({@link AbstractSqlClauses}). | ||
* This converter is used as the first action within the SQL drivers. | ||
* | ||
* @module | ||
*/ | ||
/** | ||
* Here the abstract query gets converted into the abstract SQL query. | ||
* It calls all related conversion functions and takes care of the parameter index. | ||
* This process, is also part of the ORM since here the aliases get generated and the mapping of aliases to the original fields is created. | ||
* | ||
* @param abstractQuery the abstract query to convert | ||
* @returns a format very close to actual SQL but without making assumptions about the actual SQL dialect | ||
* @returns the abstract sql query | ||
*/ | ||
declare const convertAbstractQueryToAbstractSqlQuery: (abstractQuery: AbstractQuery) => AbstractSqlQuery; | ||
declare const convertQuery: (abstractQuery: AbstractQuery) => AbstractSqlQuery; | ||
export { AbstractSqlQuery, AbstractSqlQueryOrderNode, AbstractSqlQueryWhereConditionNode, AbstractSqlQueryWhereLogicalNode, CompareValueNode, ParameterizedSQLStatement, SqlStatementSelectColumn, convertAbstractQueryToAbstractSqlQuery }; | ||
/** | ||
* 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 | ||
* context of the current query. The generated identifier is used as an alias to select columns and to join tables. | ||
* | ||
* @remarks | ||
* The uniqueness of a table or column within the schema is not enough for us, since f.e. the same table can be joined multiple times | ||
* and only with some randomness added, we can ensure that the ORM does the nesting correctly. | ||
* | ||
* @todo OracleDB has a max length of 30 characters for identifiers. Is this the right spot to | ||
* ensure that, or should that be on the DB level? | ||
*/ | ||
declare const createUniqueAlias: (identifier: string) => string; | ||
/** | ||
* @param operation | ||
* @param negate | ||
* @returns | ||
*/ | ||
declare function convertNumericOperators(operation: string, negate: boolean): string; | ||
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 }; |
@@ -1,119 +0,432 @@ | ||
// src/converter/convert-primitive.ts | ||
var convertPrimitive = (abstractPrimitive, collection) => { | ||
const statement = { | ||
// 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, abstractPrimitive, generatedAlias) => { | ||
const primitive = { | ||
type: "primitive", | ||
table: collection, | ||
column: abstractPrimitive.field | ||
column: abstractPrimitive.field, | ||
as: generatedAlias | ||
}; | ||
if (abstractPrimitive.alias) { | ||
statement.as = abstractPrimitive.alias; | ||
primitive.alias = abstractPrimitive.alias; | ||
} | ||
return statement; | ||
return primitive; | ||
}; | ||
// src/utils/param-index-generator.ts | ||
function* parameterIndexGenerator() { | ||
let index = 0; | ||
while (true) { | ||
yield index++; | ||
// src/query-converter/fields/create-join.ts | ||
var createJoin = (currentCollection, relationalField, externalCollectionAlias, fieldAlias) => { | ||
let on; | ||
if (relationalField.join.current.fields.length > 1) { | ||
on = { | ||
type: "logical", | ||
operator: "and", | ||
negate: false, | ||
childNodes: relationalField.join.current.fields.map((currentField, index) => { | ||
const externalField = relationalField.join.external.fields[index]; | ||
if (!externalField) { | ||
throw new Error(`Missing related foreign key join column for current context column "${currentField}"`); | ||
} | ||
return getJoinCondition(currentCollection, currentField, externalCollectionAlias, externalField); | ||
}) | ||
}; | ||
} else { | ||
on = getJoinCondition( | ||
currentCollection, | ||
relationalField.join.current.fields[0], | ||
externalCollectionAlias, | ||
relationalField.join.external.fields[0] | ||
); | ||
} | ||
const result = { | ||
type: "join", | ||
table: relationalField.join.external.collection, | ||
as: externalCollectionAlias, | ||
on | ||
}; | ||
if (fieldAlias) { | ||
result.alias = fieldAlias; | ||
} | ||
return result; | ||
}; | ||
function getJoinCondition(table1, column1, table2, column2) { | ||
return { | ||
type: "condition", | ||
negate: false, | ||
condition: { | ||
type: "condition-field", | ||
target: { | ||
type: "primitive", | ||
table: table1, | ||
column: column1 | ||
}, | ||
operation: "eq", | ||
compareTo: { | ||
type: "primitive", | ||
table: table2, | ||
column: column2 | ||
} | ||
} | ||
}; | ||
} | ||
// src/converter/convert-sort.ts | ||
var convertSort = (abstractSorts) => { | ||
return abstractSorts.map((abstractSort) => { | ||
return { | ||
orderBy: abstractSort.target, | ||
direction: abstractSort.direction === "descending" ? "DESC" : "ASC" | ||
// 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) | ||
}; | ||
}); | ||
} | ||
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/converter/convert-filter.ts | ||
var convertFilter = (filter, collection, generator) => { | ||
return convertFilterWithNegate(filter, collection, generator, false); | ||
}; | ||
var convertFilterWithNegate = (filter, collection, generator, negate) => { | ||
if (filter.type === "condition") { | ||
if (filter.target.type !== "primitive") { | ||
throw new Error("Only primitives are currently supported."); | ||
// 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 (filter.operation === "intersects" || filter.operation === "intersects_bounding_box") { | ||
throw new Error("The intersects operators are not yet supported."); | ||
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; | ||
} | ||
return { | ||
where: { | ||
type: "condition", | ||
negate, | ||
operation: filter.operation, | ||
target: { | ||
column: filter.target.field, | ||
table: collection, | ||
type: "primitive" | ||
}, | ||
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); | ||
return { | ||
where: { | ||
type: "logical", | ||
negate, | ||
operator, | ||
childNodes | ||
}, | ||
parameters | ||
}; | ||
} | ||
// 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) { | ||
return { | ||
where: { | ||
type: "condition", | ||
negate, | ||
condition: { | ||
type: "condition-field", | ||
operation: node.operation, | ||
target: convertPrimitive(collection, node.target), | ||
compareTo: convertPrimitive(node.compareTo.collection, node.compareTo) | ||
} | ||
}, | ||
parameters: [] | ||
}; | ||
} | ||
// src/query-converter/modifiers/filter/conditions/geo.ts | ||
function convertGeoCondition(node, collection, generator, negate) { | ||
return { | ||
where: { | ||
type: "condition", | ||
negate, | ||
condition: { | ||
type: "condition-geo", | ||
operation: node.operation, | ||
target: convertPrimitive(collection, node.target), | ||
compareTo: { | ||
type: "value", | ||
parameterIndexes: [generator.next().value] | ||
parameterIndex: generator.next().value | ||
} | ||
}, | ||
parameters: [filter.compareTo.value] | ||
}; | ||
} | ||
}, | ||
parameters: [node.compareTo] | ||
}; | ||
} | ||
// src/query-converter/modifiers/filter/conditions/string.ts | ||
function convertStringNode(node, collection, generator, negate) { | ||
return { | ||
where: { | ||
type: "condition", | ||
negate, | ||
condition: { | ||
type: node.type, | ||
operation: node.operation, | ||
target: convertPrimitive(collection, node.target), | ||
compareTo: { | ||
type: "value", | ||
parameterIndex: generator.next().value | ||
} | ||
} | ||
}, | ||
parameters: [node.compareTo] | ||
}; | ||
} | ||
// src/query-converter/modifiers/filter/conditions/number.ts | ||
function convertNumberNode(node, collection, generator, negate) { | ||
return { | ||
where: { | ||
type: "condition", | ||
negate, | ||
condition: { | ||
type: node.type, | ||
operation: node.operation, | ||
target: convertTarget(node, collection, generator), | ||
compareTo: { | ||
type: "value", | ||
parameterIndex: generator.next().value | ||
} | ||
} | ||
}, | ||
parameters: [node.compareTo] | ||
}; | ||
} | ||
// src/query-converter/modifiers/filter/conditions/set.ts | ||
function convertSetCondition(node, collection, generator, negate) { | ||
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) | ||
} | ||
} | ||
}, | ||
parameters: [...node.compareTo] | ||
}; | ||
} | ||
// src/query-converter/modifiers/filter/conditions/conditions.ts | ||
function convertCondition(condition, collection, generator, negate) { | ||
switch (condition.condition.type) { | ||
case "condition-string": | ||
return convertStringNode(condition.condition, collection, generator, negate); | ||
case "condition-number": | ||
return convertNumberNode(condition.condition, collection, generator, negate); | ||
case "condition-geo-intersects": | ||
case "condition-geo-intersects-bbox": | ||
return convertGeoCondition(condition.condition, collection, generator, negate); | ||
case "condition-set": | ||
return convertSetCondition(condition.condition, collection, generator, negate); | ||
case "condition-field": | ||
return convertFieldCondition(condition.condition, collection, negate); | ||
} | ||
} | ||
// src/query-converter/modifiers/filter/filter.ts | ||
var convertFilter = (filter, collection, generator, negate = false) => { | ||
if (filter.type === "condition") { | ||
return convertCondition(filter, collection, generator, negate); | ||
} else if (filter.type === "negate") { | ||
return convertFilterWithNegate(filter.childNode, collection, generator, !negate); | ||
return convertFilter(filter.childNode, collection, generator, !negate); | ||
} else if (filter.type === "logical") { | ||
const children = filter.childNodes.map((childNode) => convertFilter(childNode, collection, generator, false)); | ||
return convertLogical(children, filter.operator, negate); | ||
} else { | ||
const children = filter.childNodes.map( | ||
(childNode) => convertFilterWithNegate(childNode, collection, generator, false) | ||
); | ||
throw new Error(`Unknown filter type`); | ||
} | ||
}; | ||
// src/query-converter/modifiers/sort.ts | ||
var convertSort = (abstractSorts) => { | ||
return abstractSorts.map((abstractSort) => { | ||
return { | ||
where: { | ||
type: "logical", | ||
negate, | ||
operator: filter.operator, | ||
childNodes: children.map((child) => child.where) | ||
}, | ||
parameters: children.flatMap((child) => child.parameters) | ||
type: "order", | ||
orderBy: abstractSort.target, | ||
direction: abstractSort.direction === "descending" ? "DESC" : "ASC" | ||
}; | ||
} | ||
}); | ||
}; | ||
// src/converter/index.ts | ||
var convertAbstractQueryToAbstractSqlQuery = (abstractQuery) => { | ||
const statement = { | ||
select: abstractQuery.nodes.map((abstractNode) => { | ||
switch (abstractNode.type) { | ||
case "primitive": | ||
return convertPrimitive(abstractNode, abstractQuery.collection); | ||
case "fn": | ||
case "m2o": | ||
case "o2m": | ||
case "a2o": | ||
case "o2a": | ||
default: | ||
throw new Error(`Type ${abstractNode.type} hasn't been implemented yet`); | ||
} | ||
}), | ||
from: abstractQuery.collection, | ||
// src/query-converter/modifiers/modifiers.ts | ||
var convertModifiers = (modifiers, collection, idxGenerator) => { | ||
const result = { | ||
clauses: {}, | ||
parameters: [] | ||
}; | ||
const idGen = parameterIndexGenerator(); | ||
if (abstractQuery.modifiers?.filter) { | ||
const convertedFilter = convertFilter(abstractQuery.modifiers.filter, abstractQuery.collection, idGen); | ||
statement.where = convertedFilter.where; | ||
statement.parameters.push(...convertedFilter.parameters); | ||
if (modifiers?.filter) { | ||
const convertedFilter = convertFilter(modifiers.filter, collection, idxGenerator); | ||
result.clauses.where = convertedFilter.where; | ||
result.parameters.push(...convertedFilter.parameters); | ||
} | ||
if (abstractQuery.modifiers?.limit) { | ||
statement.limit = { parameterIndex: idGen.next().value }; | ||
statement.parameters.push(abstractQuery.modifiers.limit.value); | ||
if (modifiers?.limit) { | ||
result.clauses.limit = { type: "value", parameterIndex: idxGenerator.next().value }; | ||
result.parameters.push(modifiers.limit.value); | ||
} | ||
if (abstractQuery.modifiers?.offset) { | ||
statement.offset = { parameterIndex: idGen.next().value }; | ||
statement.parameters.push(abstractQuery.modifiers.offset.value); | ||
if (modifiers?.offset) { | ||
result.clauses.offset = { type: "value", parameterIndex: idxGenerator.next().value }; | ||
result.parameters.push(modifiers.offset.value); | ||
} | ||
if (abstractQuery.modifiers?.sort) { | ||
statement.order = convertSort(abstractQuery.modifiers.sort); | ||
if (modifiers?.sort) { | ||
result.clauses.order = convertSort(modifiers.sort); | ||
} | ||
return statement; | ||
return result; | ||
}; | ||
// src/query-converter/converter.ts | ||
var convertQuery = (abstractQuery) => { | ||
const idGen = parameterIndexGenerator(); | ||
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); | ||
return { | ||
clauses, | ||
parameters, | ||
aliasMapping: convertedFieldNodes.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) { | ||
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}`); | ||
} | ||
set(result, path, value); | ||
} | ||
return result; | ||
} | ||
// src/utils/numeric-operator-conversion.ts | ||
function convertNumericOperators(operation, negate) { | ||
let result = ""; | ||
switch (operation) { | ||
case "eq": | ||
result = `${negate ? "!=" : "="}`; | ||
break; | ||
case "gt": | ||
result = `${negate ? "<=" : ">"}`; | ||
break; | ||
case "gte": | ||
result = `${negate ? "<" : ">="}`; | ||
break; | ||
case "lt": | ||
result = `${negate ? ">=" : "<"}`; | ||
break; | ||
case "lte": | ||
result = `${negate ? ">" : "<="}`; | ||
break; | ||
default: | ||
throw new Error(`Unknown numeric operator: ${operation}`); | ||
} | ||
return result; | ||
} | ||
export { | ||
convertAbstractQueryToAbstractSqlQuery | ||
convertNumericOperators, | ||
convertQuery, | ||
createUniqueAlias, | ||
getOrmTransformer, | ||
transformChunk | ||
}; |
{ | ||
"name": "@directus/data-sql", | ||
"version": "0.2.1", | ||
"version": "0.3.0", | ||
"type": "module", | ||
@@ -24,17 +24,24 @@ "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", | ||
"tsup": "6.7.0", | ||
"typescript": "5.0.4", | ||
"dependency-cruiser": "13.1.4", | ||
"tsup": "7.2.0", | ||
"typescript": "5.2.2", | ||
"vitest": "0.31.1", | ||
"@directus/data": "0.2.1", | ||
"@directus/random": "0.2.2", | ||
"@directus/tsconfig": "1.0.0", | ||
"@directus/types": "10.1.3" | ||
"@directus/data": "0.3.0", | ||
"@directus/tsconfig": "1.0.1", | ||
"@directus/random": "0.2.3", | ||
"@directus/types": "11.0.0" | ||
}, | ||
"dependencies": { | ||
"lodash-es": "4.17.21" | ||
}, | ||
"scripts": { | ||
"build": "tsup src/index.ts --format=esm --dts", | ||
"dev": "tsup src/index.ts --format=esm --dts --watch", | ||
"test": "vitest --watch=false" | ||
"test": "vitest --watch=false", | ||
"depcruise": "depcruise src --include-only '^src' -x test.ts --output-type dot | dot -T svg > dependency-graph.svg" | ||
} | ||
} |
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
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
Found 1 instance in 1 package
29913
5
658
1
22
1
12
1
+ Addedlodash-es@4.17.21
+ Addedlodash-es@4.17.21(transitive)