diff --git a/api/src/database/functions/index.ts b/api/src/database/functions/index.ts deleted file mode 100644 index da505b9480..0000000000 --- a/api/src/database/functions/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Knex } from 'knex'; - -import { HelperPostgres } from './dialects/postgres'; -import { HelperMySQL } from './dialects/mysql'; -import { HelperMSSQL } from './dialects/mssql'; -import { HelperSQLite } from './dialects/sqlite'; -import { HelperOracle } from './dialects/oracle'; - -import { HelperFn } from './types'; - -export function FunctionsHelper(knex: Knex): HelperFn { - switch (knex.client.constructor.name) { - case 'Client_MySQL': - return new HelperMySQL(knex); - case 'Client_PG': - return new HelperPostgres(knex); - case 'Client_SQLite3': - return new HelperSQLite(knex); - case 'Client_Oracledb': - case 'Client_Oracle': - return new HelperOracle(knex); - case 'Client_MSSQL': - return new HelperMSSQL(knex); - default: - throw Error('Unsupported driver used: ' + knex.client.constructor.name); - } -} diff --git a/api/src/database/functions/types.ts b/api/src/database/functions/types.ts deleted file mode 100644 index 302ba629c8..0000000000 --- a/api/src/database/functions/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Knex } from 'knex'; - -export interface HelperFn { - year(table: string, column: string): Knex.Raw; - month(table: string, column: string): Knex.Raw; - week(table: string, column: string): Knex.Raw; - day(table: string, column: string): Knex.Raw; - weekday(table: string, column: string): Knex.Raw; - hour(table: string, column: string): Knex.Raw; - minute(table: string, column: string): Knex.Raw; - second(table: string, column: string): Knex.Raw; -} diff --git a/api/src/database/helpers/date.ts b/api/src/database/helpers/date.ts deleted file mode 100644 index 96cf37b4dc..0000000000 --- a/api/src/database/helpers/date.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Knex } from 'knex'; -import getDatabase from '..'; - -let dateHelper: KnexDate | undefined; - -export function getDateHelper(): KnexDate { - if (!dateHelper) { - const db = getDatabase(); - const client = db.client.config.client as string; - const constructor = { - mysql: KnexDate, - mariadb: KnexDate, - sqlite3: KnexDate_SQLITE, - pg: KnexDate, - postgres: KnexDate, - redshift: KnexDate, - mssql: KnexDate, - oracledb: KnexDate, - }[client]; - if (!constructor) { - throw new Error(`Geometry helper not implemented on ${client}.`); - } - dateHelper = new constructor(db); - } - return dateHelper; -} - -class KnexDate { - constructor(protected knex: Knex) {} - - parseDate(date: string): string { - return date; - } -} - -class KnexDate_SQLITE extends KnexDate { - parseDate(date: string): string { - const newDate = new Date(date); - return (newDate.getTime() - newDate.getTimezoneOffset() * 60 * 1000).toString(); - } -} diff --git a/api/src/database/functions/dialects/mssql.ts b/api/src/database/helpers/date/dialects/mssql.ts similarity index 86% rename from api/src/database/functions/dialects/mssql.ts rename to api/src/database/helpers/date/dialects/mssql.ts index dfb539a7e2..9f3fcfc72c 100644 --- a/api/src/database/functions/dialects/mssql.ts +++ b/api/src/database/helpers/date/dialects/mssql.ts @@ -1,13 +1,7 @@ +import { DateHelper } from '../types'; import { Knex } from 'knex'; -import { HelperFn } from '../types'; - -export class HelperMSSQL implements HelperFn { - private knex: Knex; - - constructor(knex: Knex) { - this.knex = knex; - } +export class DateHelperMSSQL extends DateHelper { year(table: string, column: string): Knex.Raw { return this.knex.raw('DATEPART(year, ??.??)', [table, column]); } diff --git a/api/src/database/functions/dialects/mysql.ts b/api/src/database/helpers/date/dialects/mysql.ts similarity index 85% rename from api/src/database/functions/dialects/mysql.ts rename to api/src/database/helpers/date/dialects/mysql.ts index c810299619..0bf4843de6 100644 --- a/api/src/database/functions/dialects/mysql.ts +++ b/api/src/database/helpers/date/dialects/mysql.ts @@ -1,13 +1,7 @@ +import { DateHelper } from '../types'; import { Knex } from 'knex'; -import { HelperFn } from '../types'; - -export class HelperMySQL implements HelperFn { - private knex: Knex; - - constructor(knex: Knex) { - this.knex = knex; - } +export class DateHelperMySQL extends DateHelper { year(table: string, column: string): Knex.Raw { return this.knex.raw('YEAR(??.??)', [table, column]); } diff --git a/api/src/database/functions/dialects/oracle.ts b/api/src/database/helpers/date/dialects/oracle.ts similarity index 86% rename from api/src/database/functions/dialects/oracle.ts rename to api/src/database/helpers/date/dialects/oracle.ts index 34dcad2f8d..0e0e512e56 100644 --- a/api/src/database/functions/dialects/oracle.ts +++ b/api/src/database/helpers/date/dialects/oracle.ts @@ -1,13 +1,7 @@ +import { DateHelper } from '../types'; import { Knex } from 'knex'; -import { HelperFn } from '../types'; - -export class HelperOracle implements HelperFn { - private knex: Knex; - - constructor(knex: Knex) { - this.knex = knex; - } +export class DateHelperOracle extends DateHelper { year(table: string, column: string): Knex.Raw { return this.knex.raw("TO_CHAR(??.??, 'IYYY')", [table, column]); } diff --git a/api/src/database/functions/dialects/postgres.ts b/api/src/database/helpers/date/dialects/postgres.ts similarity index 86% rename from api/src/database/functions/dialects/postgres.ts rename to api/src/database/helpers/date/dialects/postgres.ts index fb3fe33aec..13d1542fd3 100644 --- a/api/src/database/functions/dialects/postgres.ts +++ b/api/src/database/helpers/date/dialects/postgres.ts @@ -1,13 +1,7 @@ +import { DateHelper } from '../types'; import { Knex } from 'knex'; -import { HelperFn } from '../types'; - -export class HelperPostgres implements HelperFn { - private knex: Knex; - - constructor(knex: Knex) { - this.knex = knex; - } +export class DateHelperPostgres extends DateHelper { year(table: string, column: string): Knex.Raw { return this.knex.raw('EXTRACT(YEAR FROM ??.??)', [table, column]); } diff --git a/api/src/database/functions/dialects/sqlite.ts b/api/src/database/helpers/date/dialects/sqlite.ts similarity index 80% rename from api/src/database/functions/dialects/sqlite.ts rename to api/src/database/helpers/date/dialects/sqlite.ts index 6bfdaea473..394f698917 100644 --- a/api/src/database/functions/dialects/sqlite.ts +++ b/api/src/database/helpers/date/dialects/sqlite.ts @@ -1,13 +1,7 @@ +import { DateHelper } from '../types'; import { Knex } from 'knex'; -import { HelperFn } from '../types'; - -export class HelperSQLite implements HelperFn { - private knex: Knex; - - constructor(knex: Knex) { - this.knex = knex; - } +export class DateHelperSQLite extends DateHelper { year(table: string, column: string): Knex.Raw { return this.knex.raw("strftime('%Y', ??.??)", [table, column]); } @@ -39,4 +33,9 @@ export class HelperSQLite implements HelperFn { second(table: string, column: string): Knex.Raw { return this.knex.raw("strftime('%S', ??.??)", [table, column]); } + + parse(date: string): string { + const newDate = new Date(date); + return (newDate.getTime() - newDate.getTimezoneOffset() * 60 * 1000).toString(); + } } diff --git a/api/src/database/helpers/date/index.ts b/api/src/database/helpers/date/index.ts new file mode 100644 index 0000000000..96d6c06fac --- /dev/null +++ b/api/src/database/helpers/date/index.ts @@ -0,0 +1,6 @@ +export { DateHelperPostgres as postgres } from './dialects/postgres'; +export { DateHelperPostgres as redshift } from './dialects/postgres'; +export { DateHelperOracle as oracle } from './dialects/oracle'; +export { DateHelperSQLite as sqlite } from './dialects/sqlite'; +export { DateHelperMySQL as mysql } from './dialects/mysql'; +export { DateHelperMSSQL as mssql } from './dialects/mssql'; diff --git a/api/src/database/helpers/date/types.ts b/api/src/database/helpers/date/types.ts new file mode 100644 index 0000000000..63cdfaf701 --- /dev/null +++ b/api/src/database/helpers/date/types.ts @@ -0,0 +1,16 @@ +import { DatabaseHelper } from '../types'; +import { Knex } from 'knex'; + +export abstract class DateHelper extends DatabaseHelper { + abstract year(table: string, column: string): Knex.Raw; + abstract month(table: string, column: string): Knex.Raw; + abstract week(table: string, column: string): Knex.Raw; + abstract day(table: string, column: string): Knex.Raw; + abstract weekday(table: string, column: string): Knex.Raw; + abstract hour(table: string, column: string): Knex.Raw; + abstract minute(table: string, column: string): Knex.Raw; + abstract second(table: string, column: string): Knex.Raw; + parse(date: string): string { + return date; + } +} diff --git a/api/src/database/helpers/geometry.ts b/api/src/database/helpers/geometry.ts deleted file mode 100644 index c0cba8ae36..0000000000 --- a/api/src/database/helpers/geometry.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { Field, RawField } from '@directus/shared/types'; -import { Knex } from 'knex'; -import { stringify as geojsonToWKT, GeoJSONGeometry } from 'wellknown'; -import { getDatabaseClient } from '..'; -import getDatabase from '..'; - -let geometryHelper: KnexSpatial | undefined; - -export function getGeometryHelper(database?: Knex): KnexSpatial { - if (!geometryHelper) { - database = database ?? getDatabase(); - const client = getDatabaseClient(database); - const constructor = { - mysql: KnexSpatial_MySQL, - sqlite: KnexSpatial_SQLite, - postgres: KnexSpatial_PG, - redshift: KnexSpatial_Redshift, - mssql: KnexSpatial_MSSQL, - oracle: KnexSpatial_Oracle, - }[client]; - if (!constructor) { - throw new Error(`Geometry helper not implemented on ${client}.`); - } - geometryHelper = new constructor(database); - } - return geometryHelper!; -} - -abstract class KnexSpatial { - constructor(protected knex: Knex) {} - supported(): boolean | Promise { - return true; - } - isTrue(expression: Knex.Raw) { - return expression; - } - isFalse(expression: Knex.Raw) { - return expression.wrap('NOT ', ''); - } - createColumn(table: Knex.CreateTableBuilder, field: RawField | Field) { - const type = field.type.split('.')[1] ?? 'geometry'; - return table.specificType(field.field, type); - } - asText(table: string, column: string): Knex.Raw { - return this.knex.raw('st_astext(??.??) as ??', [table, column, column]); - } - asGeoJSON?(table: string, column: string): Knex.Raw { - return this.knex.raw('st_asgeojson(??.??) as ??', [table, column, column]); - } - fromText(text: string): Knex.Raw { - return this.knex.raw('st_geomfromtext(?, 4326)', text); - } - fromGeoJSON(geojson: GeoJSONGeometry): Knex.Raw { - return this.fromText(geojsonToWKT(geojson)); - } - _intersects(key: string, geojson: GeoJSONGeometry): Knex.Raw { - const geometry = this.fromGeoJSON(geojson); - return this.knex.raw('st_intersects(??, ?)', [key, geometry]); - } - intersects(key: string, geojson: GeoJSONGeometry): Knex.Raw { - return this.isTrue(this._intersects(key, geojson)); - } - nintersects(key: string, geojson: GeoJSONGeometry): Knex.Raw { - return this.isFalse(this._intersects(key, geojson)); - } - _intersects_bbox(key: string, geojson: GeoJSONGeometry): Knex.Raw { - const geometry = this.fromGeoJSON(geojson); - return this.knex.raw('st_intersects(??, ?)', [key, geometry]); - } - intersects_bbox(key: string, geojson: GeoJSONGeometry): Knex.Raw { - return this.isTrue(this._intersects_bbox(key, geojson)); - } - nintersects_bbox(key: string, geojson: GeoJSONGeometry): Knex.Raw { - return this.isFalse(this._intersects_bbox(key, geojson)); - } - collect(table: string, column: string): Knex.Raw { - return this.knex.raw('st_astext(st_collect(??.??))', [table, column]); - } -} - -class KnexSpatial_SQLite extends KnexSpatial { - async supported() { - const res = await this.knex.select('name').from('pragma_function_list').where({ name: 'spatialite_version' }); - return res.length > 0; - } - asGeoJSON(table: string, column: string): Knex.Raw { - return this.knex.raw('asgeojson(??.??) as ??', [table, column, column]); - } -} - -class KnexSpatial_PG extends KnexSpatial { - async supported() { - const res = await this.knex.select('oid').from('pg_proc').where({ proname: 'postgis_version' }); - return res.length > 0; - } - createColumn(table: Knex.CreateTableBuilder, field: RawField | Field) { - const type = field.type.split('.')[1] ?? 'geometry'; - return table.specificType(field.field, `geometry(${type}, 4326)`); - } - _intersects_bbox(key: string, geojson: GeoJSONGeometry): Knex.Raw { - const geometry = this.fromGeoJSON(geojson); - return this.knex.raw('?? && ?', [key, geometry]); - } -} - -class KnexSpatial_MySQL extends KnexSpatial { - collect(table: string, column: string): Knex.Raw { - return this.knex.raw( - `concat('geometrycollection(', group_concat(? separator ', '), ')'`, - this.asText(table, column) - ); - } - fromText(text: string): Knex.Raw { - return this.knex.raw('st_geomfromtext(?)', text); - } -} - -class KnexSpatial_Redshift extends KnexSpatial { - createColumn(table: Knex.CreateTableBuilder, field: RawField | Field) { - if (field.type.split('.')[1]) { - field.meta!.special = [field.type]; - } - return table.specificType(field.field, 'geometry'); - } -} - -class KnexSpatial_MSSQL extends KnexSpatial { - isTrue(expression: Knex.Raw) { - return expression.wrap(``, ` = 1`); - } - isFalse(expression: Knex.Raw) { - return expression.wrap(``, ` = 0`); - } - createColumn(table: Knex.CreateTableBuilder, field: RawField | Field) { - if (field.type.split('.')[1]) { - field.meta!.special = [field.type]; - } - return table.specificType(field.field, 'geometry'); - } - asText(table: string, column: string): Knex.Raw { - return this.knex.raw('??.??.STAsText() as ??', [table, column, column]); - } - fromText(text: string): Knex.Raw { - return this.knex.raw('geometry::STGeomFromText(?, 4326)', text); - } - _intersects(key: string, geojson: GeoJSONGeometry): Knex.Raw { - const geometry = this.fromGeoJSON(geojson); - return this.knex.raw('??.STIntersects(?)', [key, geometry]); - } - _intersects_bbox(key: string, geojson: GeoJSONGeometry): Knex.Raw { - const geometry = this.fromGeoJSON(geojson); - return this.knex.raw('??.STEnvelope().STIntersects(?.STEnvelope())', [key, geometry]); - } - collect(table: string, column: string): Knex.Raw { - return this.knex.raw('geometry::CollectionAggregate(??.??).STAsText()', [table, column]); - } - asGeoJSON: undefined; -} - -class KnexSpatial_Oracle extends KnexSpatial { - isTrue(expression: Knex.Raw) { - return expression.wrap(``, ` = 'TRUE'`); - } - isFalse(expression: Knex.Raw) { - return expression.wrap(``, ` = 'FALSE'`); - } - createColumn(table: Knex.CreateTableBuilder, field: RawField | Field) { - if (field.type.split('.')[1]) { - field.meta!.special = [field.type]; - } - return table.specificType(field.field, 'sdo_geometry'); - } - asText(table: string, column: string): Knex.Raw { - return this.knex.raw('sdo_util.to_wktgeometry(??.??) as ??', [table, column, column]); - } - asGeoJSON(table: string, column: string): Knex.Raw { - return this.knex.raw('sdo_util.to_geojson(??.??) as ??', [table, column, column]); - } - fromText(text: string): Knex.Raw { - return this.knex.raw('sdo_geometry(?, 4326)', text); - } - _intersects(key: string, geojson: GeoJSONGeometry): Knex.Raw { - const geometry = this.fromGeoJSON(geojson); - return this.knex.raw(`sdo_overlapbdyintersect(??, ?)`, [key, geometry]); - } - _intersects_bbox(key: string, geojson: GeoJSONGeometry): Knex.Raw { - const geometry = this.fromGeoJSON(geojson); - return this.knex.raw(`sdo_overlapbdyintersect(sdo_geom.sdo_mbr(??), sdo_geom.sdo_mbr(?))`, [key, geometry]); - } - collect(table: string, column: string): Knex.Raw { - return this.knex.raw(`concat('geometrycollection(', listagg(?, ', '), ')'`, this.asText(table, column)); - } -} diff --git a/api/src/database/helpers/geometry/dialects/mssql.ts b/api/src/database/helpers/geometry/dialects/mssql.ts new file mode 100644 index 0000000000..323a29fbc1 --- /dev/null +++ b/api/src/database/helpers/geometry/dialects/mssql.ts @@ -0,0 +1,36 @@ +import { GeometryHelper } from '../types'; +import { Field, RawField } from '@directus/shared/types'; +import { GeoJSONGeometry } from 'wellknown'; +import { Knex } from 'knex'; + +export class GeometryHelperMSSQL extends GeometryHelper { + isTrue(expression: Knex.Raw) { + return expression.wrap(``, ` = 1`); + } + isFalse(expression: Knex.Raw) { + return expression.wrap(``, ` = 0`); + } + createColumn(table: Knex.CreateTableBuilder, field: RawField | Field) { + if (field.type.split('.')[1]) { + field.meta!.special = [field.type]; + } + return table.specificType(field.field, 'geometry'); + } + asText(table: string, column: string): Knex.Raw { + return this.knex.raw('??.??.STAsText() as ??', [table, column, column]); + } + fromText(text: string): Knex.Raw { + return this.knex.raw('geometry::STGeomFromText(?, 4326)', text); + } + _intersects(key: string, geojson: GeoJSONGeometry): Knex.Raw { + const geometry = this.fromGeoJSON(geojson); + return this.knex.raw('??.STIntersects(?)', [key, geometry]); + } + _intersects_bbox(key: string, geojson: GeoJSONGeometry): Knex.Raw { + const geometry = this.fromGeoJSON(geojson); + return this.knex.raw('??.STEnvelope().STIntersects(?.STEnvelope())', [key, geometry]); + } + collect(table: string, column: string): Knex.Raw { + return this.knex.raw('geometry::CollectionAggregate(??.??).STAsText()', [table, column]); + } +} diff --git a/api/src/database/helpers/geometry/dialects/mysql.ts b/api/src/database/helpers/geometry/dialects/mysql.ts new file mode 100644 index 0000000000..c0acdd1d3a --- /dev/null +++ b/api/src/database/helpers/geometry/dialects/mysql.ts @@ -0,0 +1,17 @@ +import { GeometryHelper } from '../types'; +import { Knex } from 'knex'; + +export class GeometryHelperMySQL extends GeometryHelper { + collect(table: string, column: string): Knex.Raw { + return this.knex.raw( + `concat('geometrycollection(', group_concat(? separator ', '), ')'`, + this.asText(table, column) + ); + } + fromText(text: string): Knex.Raw { + return this.knex.raw('st_geomfromtext(?)', text); + } + asGeoJSON(table: string, column: string): Knex.Raw { + return this.knex.raw('st_asgeojson(??.??) as ??', [table, column, column]); + } +} diff --git a/api/src/database/helpers/geometry/dialects/oracle.ts b/api/src/database/helpers/geometry/dialects/oracle.ts new file mode 100644 index 0000000000..684d3a501b --- /dev/null +++ b/api/src/database/helpers/geometry/dialects/oracle.ts @@ -0,0 +1,39 @@ +import { GeometryHelper } from '../types'; +import { Field, RawField } from '@directus/shared/types'; +import { GeoJSONGeometry } from 'wellknown'; +import { Knex } from 'knex'; + +export class GeometryHelperOracle extends GeometryHelper { + isTrue(expression: Knex.Raw) { + return expression.wrap(``, ` = 'TRUE'`); + } + isFalse(expression: Knex.Raw) { + return expression.wrap(``, ` = 'FALSE'`); + } + createColumn(table: Knex.CreateTableBuilder, field: RawField | Field) { + if (field.type.split('.')[1]) { + field.meta!.special = [field.type]; + } + return table.specificType(field.field, 'sdo_geometry'); + } + asText(table: string, column: string): Knex.Raw { + return this.knex.raw('sdo_util.to_wktgeometry(??.??) as ??', [table, column, column]); + } + asGeoJSON(table: string, column: string): Knex.Raw { + return this.knex.raw('sdo_util.to_geojson(??.??) as ??', [table, column, column]); + } + fromText(text: string): Knex.Raw { + return this.knex.raw('sdo_geometry(?, 4326)', text); + } + _intersects(key: string, geojson: GeoJSONGeometry): Knex.Raw { + const geometry = this.fromGeoJSON(geojson); + return this.knex.raw(`sdo_overlapbdyintersect(??, ?)`, [key, geometry]); + } + _intersects_bbox(key: string, geojson: GeoJSONGeometry): Knex.Raw { + const geometry = this.fromGeoJSON(geojson); + return this.knex.raw(`sdo_overlapbdyintersect(sdo_geom.sdo_mbr(??), sdo_geom.sdo_mbr(?))`, [key, geometry]); + } + collect(table: string, column: string): Knex.Raw { + return this.knex.raw(`concat('geometrycollection(', listagg(?, ', '), ')'`, this.asText(table, column)); + } +} diff --git a/api/src/database/helpers/geometry/dialects/postgres.ts b/api/src/database/helpers/geometry/dialects/postgres.ts new file mode 100644 index 0000000000..2500693587 --- /dev/null +++ b/api/src/database/helpers/geometry/dialects/postgres.ts @@ -0,0 +1,22 @@ +import { GeometryHelper } from '../types'; +import { Field, RawField } from '@directus/shared/types'; +import { GeoJSONGeometry } from 'wellknown'; +import { Knex } from 'knex'; + +export class GeometryHelperPostgres extends GeometryHelper { + async supported() { + const res = await this.knex.select('oid').from('pg_proc').where({ proname: 'postgis_version' }); + return res.length > 0; + } + createColumn(table: Knex.CreateTableBuilder, field: RawField | Field) { + const type = field.type.split('.')[1] ?? 'geometry'; + return table.specificType(field.field, `geometry(${type}, 4326)`); + } + _intersects_bbox(key: string, geojson: GeoJSONGeometry): Knex.Raw { + const geometry = this.fromGeoJSON(geojson); + return this.knex.raw('?? && ?', [key, geometry]); + } + asGeoJSON(table: string, column: string): Knex.Raw { + return this.knex.raw('st_asgeojson(??.??) as ??', [table, column, column]); + } +} diff --git a/api/src/database/helpers/geometry/dialects/redshift.ts b/api/src/database/helpers/geometry/dialects/redshift.ts new file mode 100644 index 0000000000..6dd09b39f0 --- /dev/null +++ b/api/src/database/helpers/geometry/dialects/redshift.ts @@ -0,0 +1,15 @@ +import { GeometryHelper } from '../types'; +import { Field, RawField } from '@directus/shared/types'; +import { Knex } from 'knex'; + +export class GeometryHelperRedshift extends GeometryHelper { + createColumn(table: Knex.CreateTableBuilder, field: RawField | Field) { + if (field.type.split('.')[1]) { + field.meta!.special = [field.type]; + } + return table.specificType(field.field, 'geometry'); + } + asGeoJSON(table: string, column: string): Knex.Raw { + return this.knex.raw('st_asgeojson(??.??) as ??', [table, column, column]); + } +} diff --git a/api/src/database/helpers/geometry/dialects/sqlite.ts b/api/src/database/helpers/geometry/dialects/sqlite.ts new file mode 100644 index 0000000000..4507fdc1e0 --- /dev/null +++ b/api/src/database/helpers/geometry/dialects/sqlite.ts @@ -0,0 +1,12 @@ +import { GeometryHelper } from '../types'; +import { Knex } from 'knex'; + +export class GeometryHelperSQLite extends GeometryHelper { + async supported() { + const res = await this.knex.select('name').from('pragma_function_list').where({ name: 'spatialite_version' }); + return res.length > 0; + } + asGeoJSON(table: string, column: string): Knex.Raw { + return this.knex.raw('asgeojson(??.??) as ??', [table, column, column]); + } +} diff --git a/api/src/database/helpers/geometry/index.ts b/api/src/database/helpers/geometry/index.ts new file mode 100644 index 0000000000..cc326767c3 --- /dev/null +++ b/api/src/database/helpers/geometry/index.ts @@ -0,0 +1,6 @@ +export { GeometryHelperPostgres as postgres } from './dialects/postgres'; +export { GeometryHelperRedshift as redshift } from './dialects/redshift'; +export { GeometryHelperOracle as oracle } from './dialects/oracle'; +export { GeometryHelperSQLite as sqlite } from './dialects/sqlite'; +export { GeometryHelperMySQL as mysql } from './dialects/mysql'; +export { GeometryHelperMSSQL as mssql } from './dialects/mssql'; diff --git a/api/src/database/helpers/geometry/types.ts b/api/src/database/helpers/geometry/types.ts new file mode 100644 index 0000000000..78651f79ca --- /dev/null +++ b/api/src/database/helpers/geometry/types.ts @@ -0,0 +1,52 @@ +import { stringify as geojsonToWKT, GeoJSONGeometry } from 'wellknown'; +import { Field, RawField } from '@directus/shared/types'; +import { DatabaseHelper } from '../types'; +import { Knex } from 'knex'; + +export abstract class GeometryHelper extends DatabaseHelper { + supported(): boolean | Promise { + return true; + } + isTrue(expression: Knex.Raw) { + return expression; + } + isFalse(expression: Knex.Raw) { + return expression.wrap('NOT ', ''); + } + createColumn(table: Knex.CreateTableBuilder, field: RawField | Field) { + const type = field.type.split('.')[1] ?? 'geometry'; + return table.specificType(field.field, type); + } + asText(table: string, column: string): Knex.Raw { + return this.knex.raw('st_astext(??.??) as ??', [table, column, column]); + } + fromText(text: string): Knex.Raw { + return this.knex.raw('st_geomfromtext(?, 4326)', text); + } + fromGeoJSON(geojson: GeoJSONGeometry): Knex.Raw { + return this.fromText(geojsonToWKT(geojson)); + } + _intersects(key: string, geojson: GeoJSONGeometry): Knex.Raw { + const geometry = this.fromGeoJSON(geojson); + return this.knex.raw('st_intersects(??, ?)', [key, geometry]); + } + intersects(key: string, geojson: GeoJSONGeometry): Knex.Raw { + return this.isTrue(this._intersects(key, geojson)); + } + nintersects(key: string, geojson: GeoJSONGeometry): Knex.Raw { + return this.isFalse(this._intersects(key, geojson)); + } + _intersects_bbox(key: string, geojson: GeoJSONGeometry): Knex.Raw { + const geometry = this.fromGeoJSON(geojson); + return this.knex.raw('st_intersects(??, ?)', [key, geometry]); + } + intersects_bbox(key: string, geojson: GeoJSONGeometry): Knex.Raw { + return this.isTrue(this._intersects_bbox(key, geojson)); + } + nintersects_bbox(key: string, geojson: GeoJSONGeometry): Knex.Raw { + return this.isFalse(this._intersects_bbox(key, geojson)); + } + collect(table: string, column: string): Knex.Raw { + return this.knex.raw('st_astext(st_collect(??.??))', [table, column]); + } +} diff --git a/api/src/database/helpers/index.ts b/api/src/database/helpers/index.ts new file mode 100644 index 0000000000..62200451c1 --- /dev/null +++ b/api/src/database/helpers/index.ts @@ -0,0 +1,15 @@ +import { getDatabaseClient } from '..'; +import { Knex } from 'knex'; + +import * as dateHelpers from './date'; +import * as geometryHelpers from './geometry'; + +export function getHelpers(database: Knex) { + const client = getDatabaseClient(database); + return { + date: new dateHelpers[client](database), + st: new geometryHelpers[client](database), + }; +} + +export type Helpers = ReturnType; diff --git a/api/src/database/helpers/types.ts b/api/src/database/helpers/types.ts new file mode 100644 index 0000000000..3d8ac0964b --- /dev/null +++ b/api/src/database/helpers/types.ts @@ -0,0 +1,5 @@ +import { Knex } from 'knex'; + +export abstract class DatabaseHelper { + constructor(protected knex: Knex) {} +} diff --git a/api/src/database/index.ts b/api/src/database/index.ts index 44c4c37fce..3559b0ef86 100644 --- a/api/src/database/index.ts +++ b/api/src/database/index.ts @@ -9,7 +9,7 @@ import fse from 'fs-extra'; import path from 'path'; import { merge } from 'lodash'; import { promisify } from 'util'; -import { getGeometryHelper } from './helpers/geometry'; +import { getHelpers } from './helpers'; let database: Knex | null = null; let inspector: ReturnType | null = null; @@ -221,11 +221,11 @@ export async function validateMigrations(): Promise { */ export async function validateDatabaseExtensions(): Promise { const database = getDatabase(); - const databaseClient = getDatabaseClient(database); - const geometryHelper = getGeometryHelper(database); - const geometrySupport = await geometryHelper.supported(); + const client = getDatabaseClient(database); + const helpers = getHelpers(database); + const geometrySupport = await helpers.st.supported(); if (!geometrySupport) { - switch (databaseClient) { + switch (client) { case 'postgres': logger.warn(`PostGIS isn't installed. Geometry type support will be limited.`); break; @@ -233,7 +233,7 @@ export async function validateDatabaseExtensions(): Promise { logger.warn(`Spatialite isn't installed. Geometry type support will be limited.`); break; default: - logger.warn(`Geometry type not supported on ${databaseClient}`); + logger.warn(`Geometry type not supported on ${client}`); } } } diff --git a/api/src/database/run-ast.ts b/api/src/database/run-ast.ts index 3f9f4aaebe..a9bfc9d682 100644 --- a/api/src/database/run-ast.ts +++ b/api/src/database/run-ast.ts @@ -9,8 +9,8 @@ import { getColumn } from '../utils/get-column'; import { stripFunction } from '../utils/strip-function'; import { toArray } from '@directus/shared/utils'; import { Query } from '@directus/shared/types'; -import getDatabase from './index'; -import { getGeometryHelper } from '../database/helpers/geometry'; +import getDatabase from '.'; +import { getHelpers } from '../database/helpers'; type RunASTOptions = { /** @@ -174,7 +174,7 @@ async function parseCurrentLevel( } function getColumnPreprocessor(knex: Knex, schema: SchemaOverview, table: string) { - const helper = getGeometryHelper(); + const helpers = getHelpers(knex); return function (fieldNode: FieldNode | M2ONode): Knex.Raw { let field; @@ -192,7 +192,7 @@ function getColumnPreprocessor(knex: Knex, schema: SchemaOverview, table: string } if (field.type.startsWith('geometry')) { - return helper.asText(table, field.field); + return helpers.st.asText(table, field.field); } return getColumn(knex, table, fieldNode.name, alias); diff --git a/api/src/database/seeds/run.ts b/api/src/database/seeds/run.ts index e147198fcb..f856d47f7b 100644 --- a/api/src/database/seeds/run.ts +++ b/api/src/database/seeds/run.ts @@ -4,7 +4,7 @@ import { Knex } from 'knex'; import { isObject } from 'lodash'; import path from 'path'; import { Type, Field } from '@directus/shared/types'; -import { getGeometryHelper } from '../helpers/geometry'; +import { getHelpers } from '../helpers'; type TableSeed = { table: string; @@ -27,6 +27,7 @@ type TableSeed = { }; export default async function runSeed(database: Knex): Promise { + const helpers = getHelpers(database); const exists = await database.schema.hasTable('directus_collections'); if (exists) { @@ -57,8 +58,7 @@ export default async function runSeed(database: Knex): Promise { } else if (columnInfo.type === 'hash') { column = tableBuilder.string(columnName, 255); } else if (columnInfo.type?.startsWith('geometry')) { - const helper = getGeometryHelper(); - column = helper.createColumn(tableBuilder, { field: columnName, type: columnInfo.type } as Field); + column = helpers.st.createColumn(tableBuilder, { field: columnName, type: columnInfo.type } as Field); } else { // @ts-ignore column = tableBuilder[columnInfo.type!](columnName); diff --git a/api/src/services/fields.ts b/api/src/services/fields.ts index 1a76a63416..e86e465d9d 100644 --- a/api/src/services/fields.ts +++ b/api/src/services/fields.ts @@ -19,11 +19,12 @@ import getLocalType from '../utils/get-local-type'; import { toArray } from '@directus/shared/utils'; import { isEqual, isNil } from 'lodash'; import { RelationsService } from './relations'; -import { getGeometryHelper } from '../database/helpers/geometry'; +import { getHelpers, Helpers } from '../database/helpers'; import Keyv from 'keyv'; export class FieldsService { knex: Knex; + helpers: Helpers; accountability: Accountability | null; itemsService: ItemsService; payloadService: PayloadService; @@ -34,6 +35,7 @@ export class FieldsService { constructor(options: AbstractServiceOptions) { this.knex = options.knex || getDatabase(); + this.helpers = getHelpers(this.knex); this.schemaInspector = options.knex ? SchemaInspector(options.knex) : getSchemaInspector(); this.accountability = options.accountability || null; this.itemsService = new ItemsService('directus_fields', options); @@ -468,8 +470,7 @@ export class FieldsService { } else if (field.type === 'timestamp') { column = table.timestamp(field.field, { useTz: true }); } else if (field.type.startsWith('geometry')) { - const helper = getGeometryHelper(); - column = helper.createColumn(table, field); + column = this.helpers.st.createColumn(table, field); } else { // @ts-ignore column = table[field.type](field.field); diff --git a/api/src/services/payload.ts b/api/src/services/payload.ts index e0ce950442..b5d1ea613b 100644 --- a/api/src/services/payload.ts +++ b/api/src/services/payload.ts @@ -10,7 +10,7 @@ import { Accountability, Query } from '@directus/shared/types'; import { toArray } from '@directus/shared/utils'; import { ItemsService } from './items'; import { unflatten } from 'flat'; -import { getGeometryHelper } from '../database/helpers/geometry'; +import { getHelpers, Helpers } from '../database/helpers'; import { parse as wktToGeoJSON } from 'wellknown'; import { generateHash } from '../utils/generate-hash'; @@ -33,12 +33,14 @@ type Transformers = { export class PayloadService { accountability: Accountability | null; knex: Knex; + helpers: Helpers; collection: string; schema: SchemaOverview; constructor(collection: string, options: AbstractServiceOptions) { this.accountability = options.accountability || null; this.knex = options.knex || getDatabase(); + this.helpers = getHelpers(this.knex); this.collection = collection; this.schema = options.schema; @@ -225,12 +227,10 @@ export class PayloadService { * to check if the value is a raw instance before stringifying it in the next step. */ processGeometries>[]>(payloads: T, action: Action): T { - const helper = getGeometryHelper(); - const process = action == 'read' ? (value: any) => (typeof value === 'string' ? wktToGeoJSON(value) : value) - : (value: any) => helper.fromGeoJSON(typeof value == 'string' ? JSON.parse(value) : value); + : (value: any) => this.helpers.st.fromGeoJSON(typeof value == 'string' ? JSON.parse(value) : value); const fieldsInCollection = Object.entries(this.schema.collections[this.collection].fields); const geometryColumns = fieldsInCollection.filter(([_, field]) => field.type.startsWith('geometry')); diff --git a/api/src/utils/apply-query.ts b/api/src/utils/apply-query.ts index 99209287b9..9a83d47609 100644 --- a/api/src/utils/apply-query.ts +++ b/api/src/utils/apply-query.ts @@ -8,8 +8,7 @@ import { Aggregate, Filter, LogicalFilterAND, Query } from '@directus/shared/typ import { applyFunctionToColumnName } from './apply-function-to-column-name'; import { getColumn } from './get-column'; import { getRelationType } from './get-relation-type'; -import { getGeometryHelper } from '../database/helpers/geometry'; -import { getDateHelper } from '../database/helpers/date'; +import { getHelpers } from '../database/helpers'; const generateAlias = customAlphabet('abcdefghijklmnopqrstuvwxyz', 5); @@ -144,6 +143,7 @@ export function applyFilter( collection: string, subQuery = false ) { + const helpers = getHelpers(knex); const relations: Relation[] = schema.relations; const aliasMap: Record = {}; @@ -371,8 +371,6 @@ export function applyFilter( }); } - const dateHelper = getDateHelper(); - const [collection, field] = key.split('.'); if (collection in schema.collections && field in schema.collections[collection].fields) { @@ -380,9 +378,9 @@ export function applyFilter( if (['date', 'dateTime', 'time', 'timestamp'].includes(type)) { if (Array.isArray(compareValue)) { - compareValue = compareValue.map((val) => dateHelper.parseDate(val)); + compareValue = compareValue.map((val) => helpers.date.parse(val)); } else { - compareValue = dateHelper.parseDate(compareValue); + compareValue = helpers.date.parse(compareValue); } } } @@ -479,21 +477,19 @@ export function applyFilter( dbQuery[logical].whereNotBetween(selectionRaw, value); } - const geometryHelper = getGeometryHelper(); - if (operator == '_intersects') { - dbQuery[logical].whereRaw(geometryHelper.intersects(key, compareValue)); + dbQuery[logical].whereRaw(helpers.st.intersects(key, compareValue)); } if (operator == '_nintersects') { - dbQuery[logical].whereRaw(geometryHelper.nintersects(key, compareValue)); + dbQuery[logical].whereRaw(helpers.st.nintersects(key, compareValue)); } if (operator == '_intersects_bbox') { - dbQuery[logical].whereRaw(geometryHelper.intersects_bbox(key, compareValue)); + dbQuery[logical].whereRaw(helpers.st.intersects_bbox(key, compareValue)); } if (operator == '_nintersects_bbox') { - dbQuery[logical].whereRaw(geometryHelper.nintersects_bbox(key, compareValue)); + dbQuery[logical].whereRaw(helpers.st.nintersects_bbox(key, compareValue)); } } diff --git a/api/src/utils/get-column.ts b/api/src/utils/get-column.ts index 532c9cac2f..8cdeb49cb9 100644 --- a/api/src/utils/get-column.ts +++ b/api/src/utils/get-column.ts @@ -1,5 +1,5 @@ import { Knex } from 'knex'; -import { FunctionsHelper } from '../database/functions'; +import { getHelpers } from '../database/helpers'; import { REGEX_BETWEEN_PARENS } from '@directus/shared/constants'; import { applyFunctionToColumnName } from './apply-function-to-column-name'; @@ -19,14 +19,14 @@ export function getColumn( column: string, alias: string | false = applyFunctionToColumnName(column) ): Knex.Raw { - const fn = FunctionsHelper(knex); + const { date: fn } = getHelpers(knex); if (column.includes('(') && column.includes(')')) { const functionName = column.split('(')[0]; const columnName = column.match(REGEX_BETWEEN_PARENS)![1]; if (functionName in fn) { - const result = fn[functionName as keyof typeof fn](table, columnName); + const result = fn[functionName as keyof typeof fn](table, columnName) as Knex.Raw; if (alias) { return knex.raw(result + ' AS ??', [alias]);