mirror of
https://github.com/directus/directus.git
synced 2026-01-26 06:28:00 -05:00
Improve helpers structure (#10052)
* Improve helpers structure * Added DatabaseHelper base class * Refactor index.ts
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
6
api/src/database/helpers/date/index.ts
Normal file
6
api/src/database/helpers/date/index.ts
Normal file
@@ -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';
|
||||
16
api/src/database/helpers/date/types.ts
Normal file
16
api/src/database/helpers/date/types.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<boolean> {
|
||||
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));
|
||||
}
|
||||
}
|
||||
36
api/src/database/helpers/geometry/dialects/mssql.ts
Normal file
36
api/src/database/helpers/geometry/dialects/mssql.ts
Normal file
@@ -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]);
|
||||
}
|
||||
}
|
||||
17
api/src/database/helpers/geometry/dialects/mysql.ts
Normal file
17
api/src/database/helpers/geometry/dialects/mysql.ts
Normal file
@@ -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]);
|
||||
}
|
||||
}
|
||||
39
api/src/database/helpers/geometry/dialects/oracle.ts
Normal file
39
api/src/database/helpers/geometry/dialects/oracle.ts
Normal file
@@ -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));
|
||||
}
|
||||
}
|
||||
22
api/src/database/helpers/geometry/dialects/postgres.ts
Normal file
22
api/src/database/helpers/geometry/dialects/postgres.ts
Normal file
@@ -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]);
|
||||
}
|
||||
}
|
||||
15
api/src/database/helpers/geometry/dialects/redshift.ts
Normal file
15
api/src/database/helpers/geometry/dialects/redshift.ts
Normal file
@@ -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]);
|
||||
}
|
||||
}
|
||||
12
api/src/database/helpers/geometry/dialects/sqlite.ts
Normal file
12
api/src/database/helpers/geometry/dialects/sqlite.ts
Normal file
@@ -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]);
|
||||
}
|
||||
}
|
||||
6
api/src/database/helpers/geometry/index.ts
Normal file
6
api/src/database/helpers/geometry/index.ts
Normal file
@@ -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';
|
||||
52
api/src/database/helpers/geometry/types.ts
Normal file
52
api/src/database/helpers/geometry/types.ts
Normal file
@@ -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<boolean> {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
15
api/src/database/helpers/index.ts
Normal file
15
api/src/database/helpers/index.ts
Normal file
@@ -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<typeof getHelpers>;
|
||||
5
api/src/database/helpers/types.ts
Normal file
5
api/src/database/helpers/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export abstract class DatabaseHelper {
|
||||
constructor(protected knex: Knex) {}
|
||||
}
|
||||
@@ -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<typeof SchemaInspector> | null = null;
|
||||
@@ -221,11 +221,11 @@ export async function validateMigrations(): Promise<boolean> {
|
||||
*/
|
||||
export async function validateDatabaseExtensions(): Promise<void> {
|
||||
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<void> {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string> {
|
||||
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);
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
} 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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<T extends Partial<Record<string, any>>[]>(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'));
|
||||
|
||||
@@ -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<string, string> = {};
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
Reference in New Issue
Block a user