ts-prorm-orm 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1111 -0
- package/LICENSE +21 -0
- package/README.md +573 -0
- package/dist/audit/history-query.js +180 -0
- package/dist/audit/index.js +23 -0
- package/dist/audit/logger.js +236 -0
- package/dist/cache/cache-manager.js +84 -0
- package/dist/cache/index.js +16 -0
- package/dist/cache/redis-cluster-cache.js +557 -0
- package/dist/cli.js +2200 -0
- package/dist/compliance/audit-trail.js +68 -0
- package/dist/compliance/backup-verification.js +685 -0
- package/dist/compliance/breach-detector.js +505 -0
- package/dist/compliance/consent-record.js +227 -0
- package/dist/compliance/consent-versioning.js +331 -0
- package/dist/compliance/cross-border-log.js +530 -0
- package/dist/compliance/data-classifier.js +258 -0
- package/dist/compliance/data-lineage.js +565 -0
- package/dist/compliance/data-masker.js +303 -0
- package/dist/compliance/data-portability.js +265 -0
- package/dist/compliance/data-retention.js +195 -0
- package/dist/compliance/dsar-workflow.js +342 -0
- package/dist/compliance/field-encryption.js +223 -0
- package/dist/compliance/immutable-record.js +148 -0
- package/dist/compliance/index.js +260 -0
- package/dist/compliance/privacy-impact-assessment.js +364 -0
- package/dist/compliance/pseudonymization.js +269 -0
- package/dist/compliance/query-firewall.js +1182 -0
- package/dist/compliance/rate-limiter.js +580 -0
- package/dist/compliance/right-to-erasure.js +117 -0
- package/dist/compliance/row-level-security.js +246 -0
- package/dist/compliance/security-decorator.js +574 -0
- package/dist/compliance/security-monitor.js +525 -0
- package/dist/compliance/sensitive-data-discovery.js +476 -0
- package/dist/compliance/session-isolation.js +472 -0
- package/dist/compliance/tls-enforcer.js +108 -0
- package/dist/compliance/worm-storage.js +712 -0
- package/dist/connection-manager.js +198 -0
- package/dist/connection-pool.js +519 -0
- package/dist/decorators/audit.js +136 -0
- package/dist/decorators/belongs-to-many.js +115 -0
- package/dist/decorators/belongs-to.js +115 -0
- package/dist/decorators/check.js +435 -0
- package/dist/decorators/collate.js +329 -0
- package/dist/decorators/comment.js +205 -0
- package/dist/decorators/database-settings.js +236 -0
- package/dist/decorators/default.js +244 -0
- package/dist/decorators/encryption.js +235 -0
- package/dist/decorators/engine.js +97 -0
- package/dist/decorators/fk-constraints.js +594 -0
- package/dist/decorators/foreign-table.js +136 -0
- package/dist/decorators/generated.js +274 -0
- package/dist/decorators/has-many.js +127 -0
- package/dist/decorators/has-one.js +116 -0
- package/dist/decorators/hstore.js +129 -0
- package/dist/decorators/index.js +355 -0
- package/dist/decorators/json-column.js +83 -0
- package/dist/decorators/jsonb.js +101 -0
- package/dist/decorators/orm-decorators.js +425 -0
- package/dist/decorators/permissions.js +294 -0
- package/dist/decorators/procedure.js +210 -0
- package/dist/decorators/query-options.js +556 -0
- package/dist/decorators/range.js +167 -0
- package/dist/decorators/set-column.js +83 -0
- package/dist/decorators/spatial.js +114 -0
- package/dist/decorators/storage.js +580 -0
- package/dist/decorators/timezone.js +512 -0
- package/dist/decorators/trigger.js +90 -0
- package/dist/decorators/uuid.js +135 -0
- package/dist/decorators/view.js +258 -0
- package/dist/diagrams/chen-diagram.js +354 -0
- package/dist/diagrams/class-diagram.js +384 -0
- package/dist/diagrams/dependency-diagram.js +432 -0
- package/dist/diagrams/er-diagram.js +605 -0
- package/dist/diagrams/flow-diagram.js +394 -0
- package/dist/diagrams/gantt-diagram.js +411 -0
- package/dist/diagrams/index-diagram.js +353 -0
- package/dist/diagrams/index.js +184 -0
- package/dist/diagrams/migration-diagram.js +316 -0
- package/dist/diagrams/model-diagram.js +616 -0
- package/dist/diagrams/package-diagram.js +376 -0
- package/dist/diagrams/palette.js +85 -0
- package/dist/diagrams/relational-diagram.js +455 -0
- package/dist/diagrams/schemadoc-diagram.js +309 -0
- package/dist/diagrams/sequence-diagram.js +307 -0
- package/dist/diagrams/state-diagram.js +344 -0
- package/dist/diagrams/svg-dom.js +111 -0
- package/dist/diagrams/tree-diagram.js +250 -0
- package/dist/dialects/clickhouse/index.js +1742 -0
- package/dist/dialects/cockroachdb/index.js +4677 -0
- package/dist/dialects/cratedb/index.js +441 -0
- package/dist/dialects/databricks/index.js +517 -0
- package/dist/dialects/db2/index.js +2225 -0
- package/dist/dialects/dialect.js +720 -0
- package/dist/dialects/duckdb/index.js +2014 -0
- package/dist/dialects/exasol/index.js +357 -0
- package/dist/dialects/firebird/index.js +495 -0
- package/dist/dialects/greenplum/index.js +457 -0
- package/dist/dialects/hana/index.js +1741 -0
- package/dist/dialects/mariadb/index.js +3590 -0
- package/dist/dialects/mssql/index.js +2296 -0
- package/dist/dialects/mysql/index.js +4058 -0
- package/dist/dialects/oracle/index.js +2999 -0
- package/dist/dialects/postgres/index.js +5319 -0
- package/dist/dialects/query-stream-helper.js +152 -0
- package/dist/dialects/questdb/index.js +433 -0
- package/dist/dialects/redshift/index.js +2277 -0
- package/dist/dialects/singlestore/index.js +354 -0
- package/dist/dialects/snowflake/index.js +2082 -0
- package/dist/dialects/spanner/index.js +1768 -0
- package/dist/dialects/sqlite/index.js +3382 -0
- package/dist/dialects/tidb/index.js +377 -0
- package/dist/dialects/timescaledb/index.js +164 -0
- package/dist/dialects/trino/index.js +420 -0
- package/dist/dialects/turso/index.js +227 -0
- package/dist/dialects/vertica/index.js +317 -0
- package/dist/dialects/yugabytedb/index.js +424 -0
- package/dist/errors/index.js +472 -0
- package/dist/errors/utils.js +340 -0
- package/dist/errors.js +21 -0
- package/dist/extensions/catalog/cloud-warehouse-features.js +207 -0
- package/dist/extensions/catalog/mssql-features.js +147 -0
- package/dist/extensions/catalog/mysql-mariadb-plugins.js +229 -0
- package/dist/extensions/catalog/oracle-db2-features.js +196 -0
- package/dist/extensions/catalog/postgres-extensions.js +516 -0
- package/dist/extensions/index.js +71 -0
- package/dist/extensions/types.js +13 -0
- package/dist/foreign-data.js +173 -0
- package/dist/hooks/hooks-manager.js +350 -0
- package/dist/hooks/index.js +37 -0
- package/dist/index.js +463 -0
- package/dist/logging.js +335 -0
- package/dist/migrations/index.js +40 -0
- package/dist/migrations/migration.js +196 -0
- package/dist/migrations/migrator.js +411 -0
- package/dist/migrations/prormmigration.js +275 -0
- package/dist/migrations/query-interface.js +435 -0
- package/dist/migrations/seeder.js +353 -0
- package/dist/models/associations.js +852 -0
- package/dist/models/constraints.js +288 -0
- package/dist/models/data-types.js +2518 -0
- package/dist/models/decorators.js +445 -0
- package/dist/models/index.js +33 -0
- package/dist/models/indexes.js +531 -0
- package/dist/models/methods.js +382 -0
- package/dist/models/model-manager.js +103 -0
- package/dist/models/model.js +5349 -0
- package/dist/models/operators.js +67 -0
- package/dist/models/scopes.js +189 -0
- package/dist/models/typescript-types.js +26 -0
- package/dist/nosql/aerospike/index.js +205 -0
- package/dist/nosql/allegrograph/index.js +169 -0
- package/dist/nosql/arangodb/index.js +364 -0
- package/dist/nosql/azure-blob/index.js +206 -0
- package/dist/nosql/beanstalkd/index.js +231 -0
- package/dist/nosql/beequeue/index.js +210 -0
- package/dist/nosql/bigchaindb/index.js +195 -0
- package/dist/nosql/bigtable/index.js +224 -0
- package/dist/nosql/blazegraph/index.js +173 -0
- package/dist/nosql/bullmq/index.js +191 -0
- package/dist/nosql/cassandra/index.js +174 -0
- package/dist/nosql/chroma/index.js +190 -0
- package/dist/nosql/cloudflare-kv/index.js +220 -0
- package/dist/nosql/coherence/index.js +200 -0
- package/dist/nosql/cosmosdb/index.js +157 -0
- package/dist/nosql/couchbase/index.js +213 -0
- package/dist/nosql/dax/index.js +212 -0
- package/dist/nosql/deno-kv/index.js +206 -0
- package/dist/nosql/dgraph/index.js +171 -0
- package/dist/nosql/doris/index.js +169 -0
- package/dist/nosql/druid/index.js +162 -0
- package/dist/nosql/dynamodb/index.js +937 -0
- package/dist/nosql/elasticsearch/index.js +377 -0
- package/dist/nosql/etcd/index.js +502 -0
- package/dist/nosql/eventhubs/index.js +213 -0
- package/dist/nosql/eventstore/index.js +254 -0
- package/dist/nosql/faunadb/index.js +188 -0
- package/dist/nosql/firestore/index.js +177 -0
- package/dist/nosql/fluree/index.js +148 -0
- package/dist/nosql/fuseki/index.js +170 -0
- package/dist/nosql/gcs/index.js +172 -0
- package/dist/nosql/gearman/index.js +160 -0
- package/dist/nosql/geode/index.js +196 -0
- package/dist/nosql/graphdb/index.js +169 -0
- package/dist/nosql/graylog/index.js +188 -0
- package/dist/nosql/gridgain/index.js +171 -0
- package/dist/nosql/hazelcast/index.js +162 -0
- package/dist/nosql/hbase/index.js +230 -0
- package/dist/nosql/ignite/index.js +173 -0
- package/dist/nosql/immudb/index.js +184 -0
- package/dist/nosql/index.js +232 -0
- package/dist/nosql/infinispan/index.js +200 -0
- package/dist/nosql/influxdb/index.js +0 -0
- package/dist/nosql/kafka/index.js +234 -0
- package/dist/nosql/keyspaces/index.js +189 -0
- package/dist/nosql/kinesis/index.js +253 -0
- package/dist/nosql/leveldb/index.js +153 -0
- package/dist/nosql/lmdb/index.js +160 -0
- package/dist/nosql/loki/index.js +201 -0
- package/dist/nosql/marklogic/index.js +204 -0
- package/dist/nosql/materialize/index.js +145 -0
- package/dist/nosql/meilisearch/index.js +154 -0
- package/dist/nosql/memcached/index.js +223 -0
- package/dist/nosql/milvus/index.js +410 -0
- package/dist/nosql/minio/index.js +265 -0
- package/dist/nosql/momento/index.js +179 -0
- package/dist/nosql/mongodb/index.js +461 -0
- package/dist/nosql/nats/index.js +247 -0
- package/dist/nosql/nedb/index.js +164 -0
- package/dist/nosql/neo4j/index.js +450 -0
- package/dist/nosql/neptune/index.js +470 -0
- package/dist/nosql/nsq/index.js +200 -0
- package/dist/nosql/opensearch/index.js +186 -0
- package/dist/nosql/orientdb/index.js +175 -0
- package/dist/nosql/papertrail/index.js +200 -0
- package/dist/nosql/pinecone/index.js +0 -0
- package/dist/nosql/pinot/index.js +133 -0
- package/dist/nosql/pouchdb/index.js +172 -0
- package/dist/nosql/prometheus/index.js +174 -0
- package/dist/nosql/provendb/index.js +147 -0
- package/dist/nosql/pubsub/index.js +187 -0
- package/dist/nosql/pulsar/index.js +232 -0
- package/dist/nosql/qdrant/index.js +295 -0
- package/dist/nosql/qldb/index.js +192 -0
- package/dist/nosql/r2/index.js +281 -0
- package/dist/nosql/rabbitmq/index.js +237 -0
- package/dist/nosql/ravendb/index.js +175 -0
- package/dist/nosql/redis/index.js +607 -0
- package/dist/nosql/redpanda/index.js +237 -0
- package/dist/nosql/resque/index.js +203 -0
- package/dist/nosql/rethinkdb/index.js +232 -0
- package/dist/nosql/rocksdb/index.js +152 -0
- package/dist/nosql/rockset/index.js +126 -0
- package/dist/nosql/s3/index.js +298 -0
- package/dist/nosql/scylladb/index.js +178 -0
- package/dist/nosql/signoz/index.js +227 -0
- package/dist/nosql/sns/index.js +201 -0
- package/dist/nosql/solr/index.js +231 -0
- package/dist/nosql/splunk/index.js +227 -0
- package/dist/nosql/sqs/index.js +246 -0
- package/dist/nosql/stardog/index.js +169 -0
- package/dist/nosql/starrocks/index.js +170 -0
- package/dist/nosql/store.js +2 -0
- package/dist/nosql/sumologic/index.js +213 -0
- package/dist/nosql/surrealdb/index.js +179 -0
- package/dist/nosql/terminusdb/index.js +151 -0
- package/dist/nosql/tigergraph/index.js +469 -0
- package/dist/nosql/typesense/index.js +148 -0
- package/dist/nosql/unqlite/index.js +157 -0
- package/dist/nosql/upstash/index.js +167 -0
- package/dist/nosql/vercel-kv/index.js +166 -0
- package/dist/nosql/victoriametrics/index.js +213 -0
- package/dist/nosql/virtuoso/index.js +169 -0
- package/dist/nosql/weaviate/index.js +276 -0
- package/dist/operators/index.js +108 -0
- package/dist/operators.js +2690 -0
- package/dist/prisma-migrate/index.js +38 -0
- package/dist/prisma-migrate/migration-generator.js +125 -0
- package/dist/prisma-migrate/model-generator.js +172 -0
- package/dist/prisma-migrate/relations.js +100 -0
- package/dist/prisma-migrate/schema-parser.js +167 -0
- package/dist/prisma-migrate/type-mapper.js +40 -0
- package/dist/prorm.js +6832 -0
- package/dist/query-builders/cte-builder.js +80 -0
- package/dist/query-builders/functions/aggregate.js +390 -0
- package/dist/query-builders/functions/conditional.js +503 -0
- package/dist/query-builders/functions/datetime.js +695 -0
- package/dist/query-builders/functions/fulltext.js +439 -0
- package/dist/query-builders/functions/index.js +93 -0
- package/dist/query-builders/functions/json.js +427 -0
- package/dist/query-builders/functions/math.js +399 -0
- package/dist/query-builders/functions/string.js +518 -0
- package/dist/query-builders/functions/window.js +328 -0
- package/dist/query-builders/include-builder.js +323 -0
- package/dist/query-builders/index-expression-builder.js +242 -0
- package/dist/query-builders/index.js +161 -0
- package/dist/query-builders/insert-builder.js +164 -0
- package/dist/query-builders/model-helpers.js +38 -0
- package/dist/query-builders/order-limit-builder.js +239 -0
- package/dist/query-builders/sql-compiler.js +2040 -0
- package/dist/query-builders/subquery-builder.js +152 -0
- package/dist/query-builders/update-builder.js +182 -0
- package/dist/query-builders/view-builder.js +218 -0
- package/dist/query-builders/where-builder.js +1532 -0
- package/dist/query-interface.js +622 -0
- package/dist/query-optimizers/batch-optimizer.js +426 -0
- package/dist/query-optimizers/explain-plans.js +415 -0
- package/dist/query-optimizers/index.js +51 -0
- package/dist/query-optimizers/prepared-statement-cache.js +423 -0
- package/dist/query-optimizers/query-hints.js +438 -0
- package/dist/query-optimizers/query-optimizer.js +278 -0
- package/dist/query-optimizers/slow-query-logger.js +271 -0
- package/dist/replica-manager.js +507 -0
- package/dist/schema/index.js +15 -0
- package/dist/schema/migration-generator.js +374 -0
- package/dist/schema/schema-differ.js +549 -0
- package/dist/schema/types.js +6 -0
- package/dist/sql-constants.js +300 -0
- package/dist/sqlite-advanced.js +1047 -0
- package/dist/streams/index.js +12 -0
- package/dist/streams/transforms.js +178 -0
- package/dist/transaction.js +456 -0
- package/dist/types/index.js +180 -0
- package/dist/user-management.js +127 -0
- package/dist/utils/date.js +300 -0
- package/dist/utils/index.js +1079 -0
- package/dist/utils/string.js +161 -0
- package/dist/validators/index.js +19 -0
- package/dist/validators/validator.js +911 -0
- package/package.json +190 -0
|
@@ -0,0 +1,1532 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* WHERE clause builder for the ORM
|
|
4
|
+
* Handles complex WHERE conditions with all supported operators
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.WhereBuilder = void 0;
|
|
8
|
+
exports.createWhereBuilder = createWhereBuilder;
|
|
9
|
+
exports.buildWhereClause = buildWhereClause;
|
|
10
|
+
// Import Op from operators for Symbol comparison
|
|
11
|
+
const operators_1 = require("../operators");
|
|
12
|
+
/**
|
|
13
|
+
* Check if a value is a column reference ({ $col: 'column' })
|
|
14
|
+
*/
|
|
15
|
+
function isColumnReference(value) {
|
|
16
|
+
return (value !== null &&
|
|
17
|
+
typeof value === 'object' &&
|
|
18
|
+
'$col' in value &&
|
|
19
|
+
typeof value.$col === 'string');
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Check if a key is Op.not Symbol
|
|
23
|
+
*/
|
|
24
|
+
function isOpNot(key) {
|
|
25
|
+
return key === operators_1.Op.not;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Check if a value is a literal SQL ({ $literal: 'SQL' })
|
|
29
|
+
*/
|
|
30
|
+
function isLiteralValue(value) {
|
|
31
|
+
return (value !== null &&
|
|
32
|
+
typeof value === 'object' &&
|
|
33
|
+
'$literal' in value &&
|
|
34
|
+
typeof value.$literal === 'string');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Convert a value to SQL string if it's a special type ($col, $literal)
|
|
38
|
+
* Returns the SQL string representation or null if it's a regular value
|
|
39
|
+
*/
|
|
40
|
+
function valueToSql(value, escapeId) {
|
|
41
|
+
if (isColumnReference(value)) {
|
|
42
|
+
return escapeId(value.$col);
|
|
43
|
+
}
|
|
44
|
+
if (isLiteralValue(value)) {
|
|
45
|
+
return value.$literal;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* WhereBuilder class for building WHERE clauses
|
|
51
|
+
*/
|
|
52
|
+
class WhereBuilder {
|
|
53
|
+
constructor(options = {}) {
|
|
54
|
+
this.options = options;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Build a WHERE clause from WhereOptions
|
|
58
|
+
*/
|
|
59
|
+
build(where) {
|
|
60
|
+
const values = [];
|
|
61
|
+
if (!where || Object.keys(where).length === 0) {
|
|
62
|
+
return { sql: '', values };
|
|
63
|
+
}
|
|
64
|
+
const result = this.buildCondition(where, values);
|
|
65
|
+
return { sql: result, values };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Build a condition (can be nested)
|
|
69
|
+
*/
|
|
70
|
+
buildCondition(condition, values) {
|
|
71
|
+
if (!condition) {
|
|
72
|
+
return '';
|
|
73
|
+
}
|
|
74
|
+
// Handle primitive values (direct equality)
|
|
75
|
+
if (typeof condition !== 'object') {
|
|
76
|
+
values.push(condition);
|
|
77
|
+
return '?';
|
|
78
|
+
}
|
|
79
|
+
// Handle a bare raw-SQL literal used as a whole condition, e.g. a
|
|
80
|
+
// standalone full-text-search predicate: `where: literal('MATCH(...) AGAINST(...)')`,
|
|
81
|
+
// `Op.and: [literal('...'), { ... }]`. Without this, `{ $literal }` falls
|
|
82
|
+
// through to buildFieldConditions() and is silently misread as a field
|
|
83
|
+
// named "$literal" needing an operator, dropping the condition entirely.
|
|
84
|
+
if (isLiteralValue(condition)) {
|
|
85
|
+
return condition.$literal;
|
|
86
|
+
}
|
|
87
|
+
const cond = condition;
|
|
88
|
+
// Handle [Op.not]: { condition } - top-level negation
|
|
89
|
+
// Check if any key in the object is the Op.not Symbol
|
|
90
|
+
if (this.hasOpNotKey(cond)) {
|
|
91
|
+
return this.buildOpNotCondition(cond, values);
|
|
92
|
+
}
|
|
93
|
+
// Handle Symbol-based logical operators (Op.and, Op.or, Op.not)
|
|
94
|
+
if (this.hasLogicalOperatorSymbols(cond)) {
|
|
95
|
+
return this.buildLogicalCondition(cond, values);
|
|
96
|
+
}
|
|
97
|
+
// Handle string-based logical operators: $and, $or, $not
|
|
98
|
+
if (this.isLogicalOperator(cond)) {
|
|
99
|
+
return this.buildLogicalCondition(cond, values);
|
|
100
|
+
}
|
|
101
|
+
// Handle $where - explicit where clause with column reference
|
|
102
|
+
if ('$where' in cond) {
|
|
103
|
+
return this.buildWhereCondition(cond.$where, values);
|
|
104
|
+
}
|
|
105
|
+
// Handle field conditions
|
|
106
|
+
return this.buildFieldConditions(cond, values);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Check if the object has Op.not as a key (can be string or Symbol)
|
|
110
|
+
*/
|
|
111
|
+
hasOpNotKey(obj) {
|
|
112
|
+
// Check string keys (for backward compatibility with 'not' string)
|
|
113
|
+
if (isOpNot('not')) {
|
|
114
|
+
if ('not' in obj) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Check Symbol keys (for Op.not Symbol)
|
|
119
|
+
const symbols = Object.getOwnPropertySymbols(obj);
|
|
120
|
+
return symbols.some((sym) => sym === operators_1.Op.not);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Build a condition with Op.not - { [Op.not]: { ... } } or { not: { ... } }
|
|
124
|
+
*/
|
|
125
|
+
buildOpNotCondition(cond, values) {
|
|
126
|
+
// First check for Symbol key (Op.not)
|
|
127
|
+
const symbols = Object.getOwnPropertySymbols(cond);
|
|
128
|
+
for (const sym of symbols) {
|
|
129
|
+
if (sym === operators_1.Op.not) {
|
|
130
|
+
const notCondition = cond[sym];
|
|
131
|
+
const result = this.buildCondition(notCondition, values);
|
|
132
|
+
if (result) {
|
|
133
|
+
return `NOT (${result})`;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Also check for string 'not' key for backward compatibility
|
|
138
|
+
if ('not' in cond) {
|
|
139
|
+
const notCondition = cond['not'];
|
|
140
|
+
const result = this.buildCondition(notCondition, values);
|
|
141
|
+
if (result) {
|
|
142
|
+
return `NOT (${result})`;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return '';
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Build a $where condition with column reference
|
|
149
|
+
* { $col: 'user.id', $eq: 1 } => "user"."id" = ?
|
|
150
|
+
*/
|
|
151
|
+
buildWhereCondition(whereClause, values) {
|
|
152
|
+
const col = whereClause.$col;
|
|
153
|
+
if (!col) {
|
|
154
|
+
return '';
|
|
155
|
+
}
|
|
156
|
+
// Get the operator and value (excluding $col)
|
|
157
|
+
const operatorEntries = Object.entries(whereClause).filter(([k]) => k !== '$col');
|
|
158
|
+
if (operatorEntries.length === 0) {
|
|
159
|
+
// Just column reference, no operator - treat as equality with no value
|
|
160
|
+
return this.escapeId(col);
|
|
161
|
+
}
|
|
162
|
+
// Build the condition for each operator
|
|
163
|
+
const fieldConditions = [];
|
|
164
|
+
for (const [opKey, opValue] of operatorEntries) {
|
|
165
|
+
const result = this.buildSingleWhereOperator(col, opKey, opValue, values);
|
|
166
|
+
if (result) {
|
|
167
|
+
fieldConditions.push(result);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return fieldConditions.join(' AND ');
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Build a single where operator condition
|
|
174
|
+
* Handles special value types like $col (column references) and $literal (raw SQL)
|
|
175
|
+
*/
|
|
176
|
+
buildSingleWhereOperator(field, operator, value, values) {
|
|
177
|
+
// Check if value is a special type ($col or $literal)
|
|
178
|
+
const sqlValue = valueToSql(value, this.escapeId.bind(this));
|
|
179
|
+
switch (operator) {
|
|
180
|
+
case '$eq':
|
|
181
|
+
if (sqlValue !== null) {
|
|
182
|
+
return `${this.escapeId(field)} = ${sqlValue}`;
|
|
183
|
+
}
|
|
184
|
+
values.push(value);
|
|
185
|
+
return `${this.escapeId(field)} = ?`;
|
|
186
|
+
case '$ne':
|
|
187
|
+
if (sqlValue !== null) {
|
|
188
|
+
return `${this.escapeId(field)} != ${sqlValue}`;
|
|
189
|
+
}
|
|
190
|
+
values.push(value);
|
|
191
|
+
return `${this.escapeId(field)} != ?`;
|
|
192
|
+
case '$gt':
|
|
193
|
+
if (sqlValue !== null) {
|
|
194
|
+
return `${this.escapeId(field)} > ${sqlValue}`;
|
|
195
|
+
}
|
|
196
|
+
values.push(value);
|
|
197
|
+
return `${this.escapeId(field)} > ?`;
|
|
198
|
+
case '$gte':
|
|
199
|
+
if (sqlValue !== null) {
|
|
200
|
+
return `${this.escapeId(field)} >= ${sqlValue}`;
|
|
201
|
+
}
|
|
202
|
+
values.push(value);
|
|
203
|
+
return `${this.escapeId(field)} >= ?`;
|
|
204
|
+
case '$lt':
|
|
205
|
+
if (sqlValue !== null) {
|
|
206
|
+
return `${this.escapeId(field)} < ${sqlValue}`;
|
|
207
|
+
}
|
|
208
|
+
values.push(value);
|
|
209
|
+
return `${this.escapeId(field)} < ?`;
|
|
210
|
+
case '$lte':
|
|
211
|
+
if (sqlValue !== null) {
|
|
212
|
+
return `${this.escapeId(field)} <= ${sqlValue}`;
|
|
213
|
+
}
|
|
214
|
+
values.push(value);
|
|
215
|
+
return `${this.escapeId(field)} <= ?`;
|
|
216
|
+
case '$like':
|
|
217
|
+
if (sqlValue !== null) {
|
|
218
|
+
return `${this.escapeId(field)} LIKE ${sqlValue}`;
|
|
219
|
+
}
|
|
220
|
+
values.push(value);
|
|
221
|
+
return `${this.escapeId(field)} LIKE ?`;
|
|
222
|
+
case '$notLike':
|
|
223
|
+
if (sqlValue !== null) {
|
|
224
|
+
return `${this.escapeId(field)} NOT LIKE ${sqlValue}`;
|
|
225
|
+
}
|
|
226
|
+
values.push(value);
|
|
227
|
+
return `${this.escapeId(field)} NOT LIKE ?`;
|
|
228
|
+
case '$iLike':
|
|
229
|
+
if (sqlValue !== null) {
|
|
230
|
+
return `${this.escapeId(field)} ILIKE ${sqlValue}`;
|
|
231
|
+
}
|
|
232
|
+
values.push(value);
|
|
233
|
+
return `${this.escapeId(field)} ILIKE ?`;
|
|
234
|
+
case '$notILike':
|
|
235
|
+
if (sqlValue !== null) {
|
|
236
|
+
return `${this.escapeId(field)} NOT ILIKE ${sqlValue}`;
|
|
237
|
+
}
|
|
238
|
+
values.push(value);
|
|
239
|
+
return `${this.escapeId(field)} NOT ILIKE ?`;
|
|
240
|
+
case '$in':
|
|
241
|
+
if (Array.isArray(value)) {
|
|
242
|
+
// Handle special values in array
|
|
243
|
+
const processedValues = [];
|
|
244
|
+
const regularValues = [];
|
|
245
|
+
value.forEach((v) => {
|
|
246
|
+
const sv = valueToSql(v, this.escapeId.bind(this));
|
|
247
|
+
if (sv !== null) {
|
|
248
|
+
processedValues.push(sv);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
regularValues.push(v);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
const placeholders = [...processedValues, ...regularValues.map(() => '?')].join(', ');
|
|
255
|
+
values.push(...regularValues);
|
|
256
|
+
return `${this.escapeId(field)} IN (${placeholders})`;
|
|
257
|
+
}
|
|
258
|
+
return '';
|
|
259
|
+
case '$notIn':
|
|
260
|
+
if (Array.isArray(value)) {
|
|
261
|
+
// Handle special values in array
|
|
262
|
+
const processedValues = [];
|
|
263
|
+
const regularValues = [];
|
|
264
|
+
value.forEach((v) => {
|
|
265
|
+
const sv = valueToSql(v, this.escapeId.bind(this));
|
|
266
|
+
if (sv !== null) {
|
|
267
|
+
processedValues.push(sv);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
regularValues.push(v);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
const placeholders = [...processedValues, ...regularValues.map(() => '?')].join(', ');
|
|
274
|
+
values.push(...regularValues);
|
|
275
|
+
return `${this.escapeId(field)} NOT IN (${placeholders})`;
|
|
276
|
+
}
|
|
277
|
+
return '';
|
|
278
|
+
case '$between':
|
|
279
|
+
if (Array.isArray(value) && value.length === 2) {
|
|
280
|
+
const startSql = valueToSql(value[0], this.escapeId.bind(this));
|
|
281
|
+
const endSql = valueToSql(value[1], this.escapeId.bind(this));
|
|
282
|
+
if (startSql !== null && endSql !== null) {
|
|
283
|
+
return `${this.escapeId(field)} BETWEEN ${startSql} AND ${endSql}`;
|
|
284
|
+
}
|
|
285
|
+
if (startSql !== null) {
|
|
286
|
+
values.push(value[1]);
|
|
287
|
+
return `${this.escapeId(field)} BETWEEN ${startSql} AND ?`;
|
|
288
|
+
}
|
|
289
|
+
if (endSql !== null) {
|
|
290
|
+
values.push(value[0]);
|
|
291
|
+
return `${this.escapeId(field)} BETWEEN ? AND ${endSql}`;
|
|
292
|
+
}
|
|
293
|
+
values.push(value[0], value[1]);
|
|
294
|
+
return `${this.escapeId(field)} BETWEEN ? AND ?`;
|
|
295
|
+
}
|
|
296
|
+
return '';
|
|
297
|
+
case '$notBetween':
|
|
298
|
+
if (Array.isArray(value) && value.length === 2) {
|
|
299
|
+
const startSql = valueToSql(value[0], this.escapeId.bind(this));
|
|
300
|
+
const endSql = valueToSql(value[1], this.escapeId.bind(this));
|
|
301
|
+
if (startSql !== null && endSql !== null) {
|
|
302
|
+
return `${this.escapeId(field)} NOT BETWEEN ${startSql} AND ${endSql}`;
|
|
303
|
+
}
|
|
304
|
+
if (startSql !== null) {
|
|
305
|
+
values.push(value[1]);
|
|
306
|
+
return `${this.escapeId(field)} NOT BETWEEN ${startSql} AND ?`;
|
|
307
|
+
}
|
|
308
|
+
if (endSql !== null) {
|
|
309
|
+
values.push(value[0]);
|
|
310
|
+
return `${this.escapeId(field)} NOT BETWEEN ? AND ${endSql}`;
|
|
311
|
+
}
|
|
312
|
+
values.push(value[0], value[1]);
|
|
313
|
+
return `${this.escapeId(field)} NOT BETWEEN ? AND ?`;
|
|
314
|
+
}
|
|
315
|
+
return '';
|
|
316
|
+
case '$isNull':
|
|
317
|
+
return `${this.escapeId(field)} IS NULL`;
|
|
318
|
+
case '$isNotNull':
|
|
319
|
+
return `${this.escapeId(field)} IS NOT NULL`;
|
|
320
|
+
case '$col':
|
|
321
|
+
// Column reference: compare to another column
|
|
322
|
+
return `${this.escapeId(field)} = ${this.escapeId(value)}`;
|
|
323
|
+
default:
|
|
324
|
+
// Unknown operator
|
|
325
|
+
return '';
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Check if the object has Op.and, Op.or, or Op.not as keys (Symbol-based logical operators)
|
|
330
|
+
*/
|
|
331
|
+
hasLogicalOperatorSymbols(obj) {
|
|
332
|
+
const symbols = Object.getOwnPropertySymbols(obj);
|
|
333
|
+
return symbols.some((sym) => sym === operators_1.Op.and || sym === operators_1.Op.or || sym === operators_1.Op.not);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Check if the object is a logical operator container
|
|
337
|
+
*/
|
|
338
|
+
isLogicalOperator(obj) {
|
|
339
|
+
// Check for string keys ($and, $or, $not)
|
|
340
|
+
const hasStringKeys = '$and' in obj || '$or' in obj || '$not' in obj;
|
|
341
|
+
if (hasStringKeys)
|
|
342
|
+
return true;
|
|
343
|
+
// Check for Symbol keys (Op.and, Op.or, Op.not)
|
|
344
|
+
return this.hasLogicalOperatorSymbols(obj);
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Build logical conditions ($and, $or, $not) or Symbol-based (Op.and, Op.or, Op.not)
|
|
348
|
+
*/
|
|
349
|
+
buildLogicalCondition(cond, values) {
|
|
350
|
+
const conditions = [];
|
|
351
|
+
// Handle $and (string key)
|
|
352
|
+
if (cond.$and) {
|
|
353
|
+
const andConditions = cond.$and
|
|
354
|
+
.map((c) => {
|
|
355
|
+
const result = this.buildCondition(c, values);
|
|
356
|
+
return result ? `(${result})` : '';
|
|
357
|
+
})
|
|
358
|
+
.filter((s) => s);
|
|
359
|
+
if (andConditions.length > 0) {
|
|
360
|
+
conditions.push(andConditions.join(' AND '));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// Handle Op.and (Symbol key)
|
|
364
|
+
const symbolKeys = Object.getOwnPropertySymbols(cond);
|
|
365
|
+
for (const sym of symbolKeys) {
|
|
366
|
+
if (sym === operators_1.Op.and) {
|
|
367
|
+
const andValue = cond[sym];
|
|
368
|
+
const andConditions = andValue
|
|
369
|
+
.map((c) => {
|
|
370
|
+
const result = this.buildCondition(c, values);
|
|
371
|
+
return result ? `(${result})` : '';
|
|
372
|
+
})
|
|
373
|
+
.filter((s) => s);
|
|
374
|
+
if (andConditions.length > 0) {
|
|
375
|
+
conditions.push(andConditions.join(' AND '));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// Handle $or (string key)
|
|
380
|
+
if (cond.$or) {
|
|
381
|
+
const orConditions = cond.$or
|
|
382
|
+
.map((c) => {
|
|
383
|
+
const result = this.buildCondition(c, values);
|
|
384
|
+
return result ? `(${result})` : '';
|
|
385
|
+
})
|
|
386
|
+
.filter((s) => s);
|
|
387
|
+
if (orConditions.length > 0) {
|
|
388
|
+
conditions.push(orConditions.join(' OR '));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// Handle Op.or (Symbol key)
|
|
392
|
+
for (const sym of symbolKeys) {
|
|
393
|
+
if (sym === operators_1.Op.or) {
|
|
394
|
+
const orValue = cond[sym];
|
|
395
|
+
const orConditions = orValue
|
|
396
|
+
.map((c) => {
|
|
397
|
+
const result = this.buildCondition(c, values);
|
|
398
|
+
return result ? `(${result})` : '';
|
|
399
|
+
})
|
|
400
|
+
.filter((s) => s);
|
|
401
|
+
if (orConditions.length > 0) {
|
|
402
|
+
conditions.push(orConditions.join(' OR '));
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
// Handle $not (string key)
|
|
407
|
+
if (cond.$not) {
|
|
408
|
+
const result = this.buildCondition(cond.$not, values);
|
|
409
|
+
if (result) {
|
|
410
|
+
conditions.push(`NOT (${result})`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// Handle Op.not (Symbol key)
|
|
414
|
+
for (const sym of symbolKeys) {
|
|
415
|
+
if (sym === operators_1.Op.not) {
|
|
416
|
+
const notValue = cond[sym];
|
|
417
|
+
const result = this.buildCondition(notValue, values);
|
|
418
|
+
if (result) {
|
|
419
|
+
conditions.push(`NOT (${result})`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return conditions.join(' AND ');
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Check if a key is a JSON path Symbol created by Op.json
|
|
427
|
+
* JSON path symbols have descriptions like "json:$.path" or "json:path"
|
|
428
|
+
*/
|
|
429
|
+
isJsonPathSymbol(key) {
|
|
430
|
+
if (typeof key === 'symbol') {
|
|
431
|
+
const desc = key.description || '';
|
|
432
|
+
return desc.startsWith('json:');
|
|
433
|
+
}
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Extract the JSON path from a Symbol created by Op.json
|
|
438
|
+
* The path is stored in the Symbol's description after "json:"
|
|
439
|
+
*/
|
|
440
|
+
extractJsonPath(key) {
|
|
441
|
+
const desc = key.description || '';
|
|
442
|
+
// Remove the "json:" prefix to get the path
|
|
443
|
+
let path = desc.substring(5);
|
|
444
|
+
// Ensure path starts with $.
|
|
445
|
+
if (!path.startsWith('$.')) {
|
|
446
|
+
path = `$.${path}`;
|
|
447
|
+
}
|
|
448
|
+
return path;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Build field-level conditions
|
|
452
|
+
*/
|
|
453
|
+
buildFieldConditions(cond, values) {
|
|
454
|
+
const fieldConditions = [];
|
|
455
|
+
// First, handle string keys from Object.entries
|
|
456
|
+
for (const [key, value] of Object.entries(cond)) {
|
|
457
|
+
// Skip logical operators
|
|
458
|
+
if (key.startsWith('$')) {
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
if (value === null) {
|
|
462
|
+
fieldConditions.push(`${this.escapeId(key)} IS NULL`);
|
|
463
|
+
}
|
|
464
|
+
else if (typeof value === 'object') {
|
|
465
|
+
const result = this.buildOperatorCondition(key, value, values);
|
|
466
|
+
if (result) {
|
|
467
|
+
fieldConditions.push(result);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
values.push(value);
|
|
472
|
+
fieldConditions.push(`${this.escapeId(key)} = ?`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
// Also handle Symbol keys (e.g., Op.json('path') returns a Symbol used as computed property key)
|
|
476
|
+
const symbolKeys = Object.getOwnPropertySymbols(cond);
|
|
477
|
+
for (const sym of symbolKeys) {
|
|
478
|
+
const value = cond[sym];
|
|
479
|
+
// Check if it's a JSON path Symbol
|
|
480
|
+
if (this.isJsonPathSymbol(sym)) {
|
|
481
|
+
const path = this.extractJsonPath(sym);
|
|
482
|
+
// Parse the path - format is "field.path" or "$.field.path"
|
|
483
|
+
// We need to extract the column name and JSON path
|
|
484
|
+
let field;
|
|
485
|
+
let jsonPath;
|
|
486
|
+
if (path.startsWith('$.')) {
|
|
487
|
+
// Format: $.field.subfield
|
|
488
|
+
const withoutDollar = path.substring(2);
|
|
489
|
+
const dotIndex = withoutDollar.indexOf('.');
|
|
490
|
+
if (dotIndex === -1) {
|
|
491
|
+
field = withoutDollar;
|
|
492
|
+
jsonPath = '';
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
field = withoutDollar.substring(0, dotIndex);
|
|
496
|
+
jsonPath = withoutDollar.substring(dotIndex + 1);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
// Format: field.subfield
|
|
501
|
+
const dotIndex = path.indexOf('.');
|
|
502
|
+
if (dotIndex === -1) {
|
|
503
|
+
field = path;
|
|
504
|
+
jsonPath = '';
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
field = path.substring(0, dotIndex);
|
|
508
|
+
jsonPath = path.substring(dotIndex + 1);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
// Build the JSON extraction SQL
|
|
512
|
+
const jsonExtract = jsonPath
|
|
513
|
+
? `json_extract(${this.escapeId(field)}, '$.${jsonPath}')`
|
|
514
|
+
: this.escapeId(field);
|
|
515
|
+
if (value === null) {
|
|
516
|
+
fieldConditions.push(`${jsonExtract} IS NULL`);
|
|
517
|
+
}
|
|
518
|
+
else if (typeof value === 'object') {
|
|
519
|
+
// If value is an object with operators (e.g., { $gt: 5 })
|
|
520
|
+
// We need to pass the JSON extraction as the field name
|
|
521
|
+
const result = this.buildOperatorCondition(jsonExtract, value, values);
|
|
522
|
+
if (result) {
|
|
523
|
+
fieldConditions.push(result);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
// Simple comparison
|
|
528
|
+
values.push(value);
|
|
529
|
+
fieldConditions.push(`${jsonExtract} = ?`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return fieldConditions.join(' AND ');
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Build condition for a specific operator
|
|
537
|
+
*/
|
|
538
|
+
buildOperatorCondition(field, operator, values) {
|
|
539
|
+
// Helper to handle special values ($col, $literal)
|
|
540
|
+
const handleValue = (value) => {
|
|
541
|
+
return valueToSql(value, this.escapeId.bind(this));
|
|
542
|
+
};
|
|
543
|
+
// Handle Op.not Symbol as key: { [Op.not]: 'active' } => field != 'active'
|
|
544
|
+
const symbolKeys = Object.getOwnPropertySymbols(operator);
|
|
545
|
+
for (const sym of symbolKeys) {
|
|
546
|
+
if (sym === operators_1.Op.not) {
|
|
547
|
+
const notValue = operator[sym];
|
|
548
|
+
const sqlValue = handleValue(notValue);
|
|
549
|
+
if (sqlValue !== null) {
|
|
550
|
+
return `${this.escapeId(field)} != ${sqlValue}`;
|
|
551
|
+
}
|
|
552
|
+
values.push(notValue);
|
|
553
|
+
return `${this.escapeId(field)} != ?`;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// Also handle 'not' as string key for backward compatibility
|
|
557
|
+
if ('not' in operator && !operator.$not) {
|
|
558
|
+
const notValue = operator.not;
|
|
559
|
+
const sqlValue = handleValue(notValue);
|
|
560
|
+
if (sqlValue !== null) {
|
|
561
|
+
return `${this.escapeId(field)} != ${sqlValue}`;
|
|
562
|
+
}
|
|
563
|
+
values.push(notValue);
|
|
564
|
+
return `${this.escapeId(field)} != ?`;
|
|
565
|
+
}
|
|
566
|
+
// $eq - equality
|
|
567
|
+
if (operator.$eq !== undefined) {
|
|
568
|
+
const sqlValue = handleValue(operator.$eq);
|
|
569
|
+
if (sqlValue !== null) {
|
|
570
|
+
return `${this.escapeId(field)} = ${sqlValue}`;
|
|
571
|
+
}
|
|
572
|
+
values.push(operator.$eq);
|
|
573
|
+
return `${this.escapeId(field)} = ?`;
|
|
574
|
+
}
|
|
575
|
+
// $ne - not equal
|
|
576
|
+
if (operator.$ne !== undefined) {
|
|
577
|
+
const sqlValue = handleValue(operator.$ne);
|
|
578
|
+
if (sqlValue !== null) {
|
|
579
|
+
return `${this.escapeId(field)} != ${sqlValue}`;
|
|
580
|
+
}
|
|
581
|
+
values.push(operator.$ne);
|
|
582
|
+
return `${this.escapeId(field)} != ?`;
|
|
583
|
+
}
|
|
584
|
+
// $gt - greater than
|
|
585
|
+
if (operator.$gt !== undefined) {
|
|
586
|
+
const sqlValue = handleValue(operator.$gt);
|
|
587
|
+
if (sqlValue !== null) {
|
|
588
|
+
return `${this.escapeId(field)} > ${sqlValue}`;
|
|
589
|
+
}
|
|
590
|
+
values.push(operator.$gt);
|
|
591
|
+
return `${this.escapeId(field)} > ?`;
|
|
592
|
+
}
|
|
593
|
+
// $gte - greater than or equal
|
|
594
|
+
if (operator.$gte !== undefined) {
|
|
595
|
+
const sqlValue = handleValue(operator.$gte);
|
|
596
|
+
if (sqlValue !== null) {
|
|
597
|
+
return `${this.escapeId(field)} >= ${sqlValue}`;
|
|
598
|
+
}
|
|
599
|
+
values.push(operator.$gte);
|
|
600
|
+
return `${this.escapeId(field)} >= ?`;
|
|
601
|
+
}
|
|
602
|
+
// $lt - less than
|
|
603
|
+
if (operator.$lt !== undefined) {
|
|
604
|
+
const sqlValue = handleValue(operator.$lt);
|
|
605
|
+
if (sqlValue !== null) {
|
|
606
|
+
return `${this.escapeId(field)} < ${sqlValue}`;
|
|
607
|
+
}
|
|
608
|
+
values.push(operator.$lt);
|
|
609
|
+
return `${this.escapeId(field)} < ?`;
|
|
610
|
+
}
|
|
611
|
+
// $lte - less than or equal
|
|
612
|
+
if (operator.$lte !== undefined) {
|
|
613
|
+
const sqlValue = handleValue(operator.$lte);
|
|
614
|
+
if (sqlValue !== null) {
|
|
615
|
+
return `${this.escapeId(field)} <= ${sqlValue}`;
|
|
616
|
+
}
|
|
617
|
+
values.push(operator.$lte);
|
|
618
|
+
return `${this.escapeId(field)} <= ?`;
|
|
619
|
+
}
|
|
620
|
+
// $like - LIKE operator
|
|
621
|
+
if (operator.$like !== undefined) {
|
|
622
|
+
const sqlValue = handleValue(operator.$like);
|
|
623
|
+
if (sqlValue !== null) {
|
|
624
|
+
return `${this.escapeId(field)} LIKE ${sqlValue}`;
|
|
625
|
+
}
|
|
626
|
+
values.push(operator.$like);
|
|
627
|
+
return `${this.escapeId(field)} LIKE ?`;
|
|
628
|
+
}
|
|
629
|
+
// $notLike - NOT LIKE operator
|
|
630
|
+
if (operator.$notLike !== undefined) {
|
|
631
|
+
const sqlValue = handleValue(operator.$notLike);
|
|
632
|
+
if (sqlValue !== null) {
|
|
633
|
+
return `${this.escapeId(field)} NOT LIKE ${sqlValue}`;
|
|
634
|
+
}
|
|
635
|
+
values.push(operator.$notLike);
|
|
636
|
+
return `${this.escapeId(field)} NOT LIKE ?`;
|
|
637
|
+
}
|
|
638
|
+
// $in - IN operator
|
|
639
|
+
if (operator.$in !== undefined) {
|
|
640
|
+
const inValues = operator.$in;
|
|
641
|
+
// Handle subquery (literal SQL string starting with SELECT)
|
|
642
|
+
if (typeof inValues === 'string' && inValues.toUpperCase().startsWith('SELECT')) {
|
|
643
|
+
return `${this.escapeId(field)} IN (${inValues})`;
|
|
644
|
+
}
|
|
645
|
+
// Handle literal SQL subquery object
|
|
646
|
+
if (typeof inValues === 'object' && inValues !== null && '$literal' in inValues) {
|
|
647
|
+
return `${this.escapeId(field)} IN (${inValues.$literal})`;
|
|
648
|
+
}
|
|
649
|
+
if (!Array.isArray(inValues) || inValues.length === 0) {
|
|
650
|
+
return '1=0'; // Always false for empty IN
|
|
651
|
+
}
|
|
652
|
+
// Handle array of special values
|
|
653
|
+
const placeholders = [];
|
|
654
|
+
for (const item of inValues) {
|
|
655
|
+
const itemSql = handleValue(item);
|
|
656
|
+
if (itemSql !== null) {
|
|
657
|
+
placeholders.push(itemSql);
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
placeholders.push('?');
|
|
661
|
+
values.push(item);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return `${this.escapeId(field)} IN (${placeholders.join(', ')})`;
|
|
665
|
+
}
|
|
666
|
+
// $notIn - NOT IN operator
|
|
667
|
+
if (operator.$notIn !== undefined) {
|
|
668
|
+
const notInValues = operator.$notIn;
|
|
669
|
+
// Handle subquery (literal SQL string starting with SELECT)
|
|
670
|
+
if (typeof notInValues === 'string' &&
|
|
671
|
+
notInValues.toUpperCase().startsWith('SELECT')) {
|
|
672
|
+
return `${this.escapeId(field)} NOT IN (${notInValues})`;
|
|
673
|
+
}
|
|
674
|
+
// Handle literal SQL subquery object
|
|
675
|
+
if (typeof notInValues === 'object' &&
|
|
676
|
+
notInValues !== null &&
|
|
677
|
+
'$literal' in notInValues) {
|
|
678
|
+
return `${this.escapeId(field)} NOT IN (${notInValues.$literal})`;
|
|
679
|
+
}
|
|
680
|
+
if (!Array.isArray(notInValues) || notInValues.length === 0) {
|
|
681
|
+
return '1=1'; // Always true for empty NOT IN
|
|
682
|
+
}
|
|
683
|
+
// Handle array of special values
|
|
684
|
+
const placeholders = [];
|
|
685
|
+
for (const item of notInValues) {
|
|
686
|
+
const itemSql = handleValue(item);
|
|
687
|
+
if (itemSql !== null) {
|
|
688
|
+
placeholders.push(itemSql);
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
placeholders.push('?');
|
|
692
|
+
values.push(item);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return `${this.escapeId(field)} NOT IN (${placeholders.join(', ')})`;
|
|
696
|
+
}
|
|
697
|
+
// $between - BETWEEN operator (inclusive)
|
|
698
|
+
if (operator.$between !== undefined) {
|
|
699
|
+
const between = operator.$between;
|
|
700
|
+
const startSql = handleValue(between[0]);
|
|
701
|
+
const endSql = handleValue(between[1]);
|
|
702
|
+
if (startSql !== null && endSql !== null) {
|
|
703
|
+
return `${this.escapeId(field)} BETWEEN ${startSql} AND ${endSql}`;
|
|
704
|
+
}
|
|
705
|
+
if (startSql !== null) {
|
|
706
|
+
values.push(between[1]);
|
|
707
|
+
return `${this.escapeId(field)} BETWEEN ${startSql} AND ?`;
|
|
708
|
+
}
|
|
709
|
+
if (endSql !== null) {
|
|
710
|
+
values.push(between[0]);
|
|
711
|
+
return `${this.escapeId(field)} BETWEEN ? AND ${endSql}`;
|
|
712
|
+
}
|
|
713
|
+
values.push(between[0], between[1]);
|
|
714
|
+
return `${this.escapeId(field)} BETWEEN ? AND ?`;
|
|
715
|
+
}
|
|
716
|
+
// $exists - EXISTS subquery operator
|
|
717
|
+
if (operator.$exists !== undefined) {
|
|
718
|
+
const subquery = operator.$exists;
|
|
719
|
+
// Handle string subquery
|
|
720
|
+
if (typeof subquery === 'string') {
|
|
721
|
+
return `EXISTS (${subquery})`;
|
|
722
|
+
}
|
|
723
|
+
// Handle literal SQL subquery
|
|
724
|
+
if (typeof subquery === 'object' && subquery !== null && '$literal' in subquery) {
|
|
725
|
+
return `EXISTS (${subquery.$literal})`;
|
|
726
|
+
}
|
|
727
|
+
return '1=0'; // Default false if not a valid subquery
|
|
728
|
+
}
|
|
729
|
+
// $notExists - NOT EXISTS subquery operator
|
|
730
|
+
if (operator.$notExists !== undefined) {
|
|
731
|
+
const subquery = operator.$notExists;
|
|
732
|
+
// Handle string subquery
|
|
733
|
+
if (typeof subquery === 'string') {
|
|
734
|
+
return `NOT EXISTS (${subquery})`;
|
|
735
|
+
}
|
|
736
|
+
// Handle literal SQL subquery
|
|
737
|
+
if (typeof subquery === 'object' && subquery !== null && '$literal' in subquery) {
|
|
738
|
+
return `NOT EXISTS (${subquery.$literal})`;
|
|
739
|
+
}
|
|
740
|
+
return '1=1'; // Default true if not a valid subquery
|
|
741
|
+
}
|
|
742
|
+
// $notBetween - NOT BETWEEN operator
|
|
743
|
+
if (operator.$notBetween !== undefined) {
|
|
744
|
+
const notBetween = operator.$notBetween;
|
|
745
|
+
const startSql = handleValue(notBetween[0]);
|
|
746
|
+
const endSql = handleValue(notBetween[1]);
|
|
747
|
+
if (startSql !== null && endSql !== null) {
|
|
748
|
+
return `${this.escapeId(field)} NOT BETWEEN ${startSql} AND ${endSql}`;
|
|
749
|
+
}
|
|
750
|
+
if (startSql !== null) {
|
|
751
|
+
values.push(notBetween[1]);
|
|
752
|
+
return `${this.escapeId(field)} NOT BETWEEN ${startSql} AND ?`;
|
|
753
|
+
}
|
|
754
|
+
if (endSql !== null) {
|
|
755
|
+
values.push(notBetween[0]);
|
|
756
|
+
return `${this.escapeId(field)} NOT BETWEEN ? AND ${endSql}`;
|
|
757
|
+
}
|
|
758
|
+
values.push(notBetween[0], notBetween[1]);
|
|
759
|
+
return `${this.escapeId(field)} NOT BETWEEN ? AND ?`;
|
|
760
|
+
}
|
|
761
|
+
// $isNull - IS NULL operator
|
|
762
|
+
if (operator.$isNull !== undefined) {
|
|
763
|
+
if (operator.$isNull) {
|
|
764
|
+
return `${this.escapeId(field)} IS NULL`;
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
return `${this.escapeId(field)} IS NOT NULL`;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
// $isNotNull - IS NOT NULL operator (shorthand)
|
|
771
|
+
if (operator.$isNotNull !== undefined) {
|
|
772
|
+
if (operator.$isNotNull) {
|
|
773
|
+
return `${this.escapeId(field)} IS NOT NULL`;
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
return `${this.escapeId(field)} IS NULL`;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
// $regexp - REGEXP operator
|
|
780
|
+
if (operator.$regexp !== undefined) {
|
|
781
|
+
const sqlValue = handleValue(operator.$regexp);
|
|
782
|
+
if (sqlValue !== null) {
|
|
783
|
+
return `${this.escapeId(field)} REGEXP ${sqlValue}`;
|
|
784
|
+
}
|
|
785
|
+
values.push(operator.$regexp);
|
|
786
|
+
return `${this.escapeId(field)} REGEXP ?`;
|
|
787
|
+
}
|
|
788
|
+
// $notRegexp - NOT REGEXP operator
|
|
789
|
+
if (operator.$notRegexp !== undefined) {
|
|
790
|
+
const sqlValue = handleValue(operator.$notRegexp);
|
|
791
|
+
if (sqlValue !== null) {
|
|
792
|
+
return `${this.escapeId(field)} NOT REGEXP ${sqlValue}`;
|
|
793
|
+
}
|
|
794
|
+
values.push(operator.$notRegexp);
|
|
795
|
+
return `${this.escapeId(field)} NOT REGEXP ?`;
|
|
796
|
+
}
|
|
797
|
+
// $json - JSON column query operator
|
|
798
|
+
if (operator.$json !== undefined) {
|
|
799
|
+
const jsonValue = operator.$json;
|
|
800
|
+
if (typeof jsonValue === 'object' && jsonValue !== null) {
|
|
801
|
+
// Handle { path: 'key.subkey', value: 'something' }
|
|
802
|
+
if ('path' in jsonValue && 'value' in jsonValue) {
|
|
803
|
+
const path = jsonValue.path;
|
|
804
|
+
const normalizedPath = path.startsWith('$.') ? path : `$.${path}`;
|
|
805
|
+
values.push(jsonValue.value);
|
|
806
|
+
return `${this.buildJsonExtract(field, normalizedPath)} = ?`;
|
|
807
|
+
}
|
|
808
|
+
// Handle nested JSON query { key: value } - converts to JSON containment
|
|
809
|
+
values.push(JSON.stringify(jsonValue));
|
|
810
|
+
return `${this.escapeId(field)} = ?`;
|
|
811
|
+
}
|
|
812
|
+
// Handle plain value for JSON equality
|
|
813
|
+
values.push(JSON.stringify(jsonValue));
|
|
814
|
+
return `${this.escapeId(field)} = ?`;
|
|
815
|
+
}
|
|
816
|
+
// $jsonPath - JSON path query operator (Op.jsonPath('field', 'path'))
|
|
817
|
+
// Expected format: { $jsonPath: { field: 'column', path: 'key.subkey' } }
|
|
818
|
+
if (operator.$jsonPath !== undefined) {
|
|
819
|
+
const jsonPathValue = operator.$jsonPath;
|
|
820
|
+
if (typeof jsonPathValue === 'object' && jsonPathValue !== null && 'path' in jsonPathValue) {
|
|
821
|
+
const path = jsonPathValue.path;
|
|
822
|
+
const normalizedPath = path.startsWith('$.') ? path : `$.${path}`;
|
|
823
|
+
const compareValue = jsonPathValue.value;
|
|
824
|
+
if (compareValue !== undefined) {
|
|
825
|
+
// Value comparison: json_extract(field, '$.path') = ?
|
|
826
|
+
values.push(compareValue);
|
|
827
|
+
return `${this.buildJsonExtract(field, normalizedPath)} = ?`;
|
|
828
|
+
}
|
|
829
|
+
// No value - just check if path exists (IS NOT NULL)
|
|
830
|
+
return `${this.buildJsonExtract(field, normalizedPath)} IS NOT NULL`;
|
|
831
|
+
}
|
|
832
|
+
// Fallback
|
|
833
|
+
return '';
|
|
834
|
+
}
|
|
835
|
+
// Handle Symbol-based JSON operators (Op.json('path') creates Symbol.for('json:path'))
|
|
836
|
+
// These appear as keys starting with '$json:' in the operator object
|
|
837
|
+
for (const key of Object.keys(operator)) {
|
|
838
|
+
if (key.startsWith('$json:')) {
|
|
839
|
+
const jsonPath = key.substring(6); // Remove '$json:' prefix
|
|
840
|
+
const normalizedPath = jsonPath.startsWith('$.') ? jsonPath : `$.${jsonPath}`;
|
|
841
|
+
const compareValue = operator[key];
|
|
842
|
+
if (compareValue !== undefined) {
|
|
843
|
+
values.push(compareValue);
|
|
844
|
+
return `${this.buildJsonExtract(field, normalizedPath)} = ?`;
|
|
845
|
+
}
|
|
846
|
+
// No value - check if path exists
|
|
847
|
+
return `${this.buildJsonExtract(field, normalizedPath)} IS NOT NULL`;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
// $contains - JSON contains operator (for objects/arrays)
|
|
851
|
+
// PostgreSQL: @>, MySQL: JSON_CONTAINS, SQLite: json_each
|
|
852
|
+
if (operator.$contains !== undefined) {
|
|
853
|
+
const containsValue = operator.$contains;
|
|
854
|
+
values.push(JSON.stringify(containsValue));
|
|
855
|
+
// Use JSON_EXTRACT for cross-database compatibility
|
|
856
|
+
// For exact match, we compare the full JSON
|
|
857
|
+
return `${this.escapeId(field)} = ?`;
|
|
858
|
+
}
|
|
859
|
+
// $containedBy - JSON contained by operator
|
|
860
|
+
// PostgreSQL: <@
|
|
861
|
+
if (operator.$containedBy !== undefined) {
|
|
862
|
+
const containedValue = operator.$containedBy;
|
|
863
|
+
values.push(JSON.stringify(containedValue));
|
|
864
|
+
// Cross-database: check if the column's JSON is contained by the value
|
|
865
|
+
return `${this.escapeId(field)} @> ?`;
|
|
866
|
+
}
|
|
867
|
+
// $overlap - JSON overlap (arrays) - PostgreSQL: &&
|
|
868
|
+
if (operator.$overlap !== undefined) {
|
|
869
|
+
const overlapValue = operator.$overlap;
|
|
870
|
+
values.push(JSON.stringify(overlapValue));
|
|
871
|
+
// Cross-database: check array overlap
|
|
872
|
+
return `${this.escapeId(field)} @> ?`;
|
|
873
|
+
}
|
|
874
|
+
// $arrayContains - PostgreSQL array contains operator (@>)
|
|
875
|
+
// Only supported in PostgreSQL
|
|
876
|
+
if (operator.$arrayContains !== undefined) {
|
|
877
|
+
const containsValue = operator.$arrayContains;
|
|
878
|
+
if (this.options.dialectName !== 'postgres' && this.options.dialectName !== 'postgresql') {
|
|
879
|
+
// Array operators only work in PostgreSQL
|
|
880
|
+
throw new Error('Array operators are only supported in PostgreSQL');
|
|
881
|
+
}
|
|
882
|
+
const arraySql = this.buildArrayLiteral(containsValue);
|
|
883
|
+
return `${this.escapeId(field)} @> ${arraySql}`;
|
|
884
|
+
}
|
|
885
|
+
// $arrayContainedBy - PostgreSQL array contained by operator (<@)
|
|
886
|
+
// Only supported in PostgreSQL
|
|
887
|
+
if (operator.$arrayContainedBy !== undefined) {
|
|
888
|
+
const containedValue = operator.$arrayContainedBy;
|
|
889
|
+
if (this.options.dialectName !== 'postgres' && this.options.dialectName !== 'postgresql') {
|
|
890
|
+
throw new Error('Array operators are only supported in PostgreSQL');
|
|
891
|
+
}
|
|
892
|
+
const arraySql = this.buildArrayLiteral(containedValue);
|
|
893
|
+
return `${this.escapeId(field)} <@ ${arraySql}`;
|
|
894
|
+
}
|
|
895
|
+
// $arrayOverlaps - PostgreSQL array overlaps operator (&&)
|
|
896
|
+
// Only supported in PostgreSQL
|
|
897
|
+
if (operator.$arrayOverlaps !== undefined) {
|
|
898
|
+
const overlapValue = operator.$arrayOverlaps;
|
|
899
|
+
if (this.options.dialectName !== 'postgres' && this.options.dialectName !== 'postgresql') {
|
|
900
|
+
throw new Error('Array operators are only supported in PostgreSQL');
|
|
901
|
+
}
|
|
902
|
+
const arraySql = this.buildArrayLiteral(overlapValue);
|
|
903
|
+
return `${this.escapeId(field)} && ${arraySql}`;
|
|
904
|
+
}
|
|
905
|
+
// $arrayAny - PostgreSQL array ANY operator
|
|
906
|
+
// Only supported in PostgreSQL
|
|
907
|
+
if (operator.$arrayAny !== undefined) {
|
|
908
|
+
const anyValue = operator.$arrayAny;
|
|
909
|
+
if (this.options.dialectName !== 'postgres' && this.options.dialectName !== 'postgresql') {
|
|
910
|
+
throw new Error('Array operators are only supported in PostgreSQL');
|
|
911
|
+
}
|
|
912
|
+
const escapedValue = this.options.dialect?.escape
|
|
913
|
+
? this.options.dialect.escape(anyValue)
|
|
914
|
+
: this.escapeValue(anyValue);
|
|
915
|
+
return `${escapedValue} = ANY(${this.escapeId(field)})`;
|
|
916
|
+
}
|
|
917
|
+
// $arrayAll - PostgreSQL array ALL operator
|
|
918
|
+
// Only supported in PostgreSQL
|
|
919
|
+
if (operator.$arrayAll !== undefined) {
|
|
920
|
+
const allValue = operator.$arrayAll;
|
|
921
|
+
if (this.options.dialectName !== 'postgres' && this.options.dialectName !== 'postgresql') {
|
|
922
|
+
throw new Error('Array operators are only supported in PostgreSQL');
|
|
923
|
+
}
|
|
924
|
+
// For array ALL, we compare: value > ALL(array_column)
|
|
925
|
+
// This checks if value is greater than all elements in the array
|
|
926
|
+
const escapedValue = this.options.dialect?.escape
|
|
927
|
+
? this.options.dialect.escape(allValue)
|
|
928
|
+
: this.escapeValue(allValue);
|
|
929
|
+
// Use the comparison operator from the inner operator if provided
|
|
930
|
+
if (typeof allValue === 'object' && allValue !== null && '$gt' in allValue) {
|
|
931
|
+
const compValue = allValue.$gt;
|
|
932
|
+
const compEscaped = this.options.dialect?.escape
|
|
933
|
+
? this.options.dialect.escape(compValue)
|
|
934
|
+
: this.escapeValue(compValue);
|
|
935
|
+
return `${compEscaped} > ALL(${this.escapeId(field)})`;
|
|
936
|
+
}
|
|
937
|
+
return `${escapedValue} = ALL(${this.escapeId(field)})`;
|
|
938
|
+
}
|
|
939
|
+
// $keyExists - Check if key exists in JSON
|
|
940
|
+
// PostgreSQL: ? (jsonb_exists), MySQL: JSON_CONTAINS_PATH, SQLite: json_each
|
|
941
|
+
if (operator.$keyExists !== undefined) {
|
|
942
|
+
const keyPath = operator.$keyExists;
|
|
943
|
+
// Use json_extract for SQLite/MySQL and ->> for PostgreSQL style comparison
|
|
944
|
+
// Check if the key path returns non-null
|
|
945
|
+
return `json_extract(${this.escapeId(field)}, '$.${keyPath}') IS NOT NULL`;
|
|
946
|
+
}
|
|
947
|
+
// $jsonContains - JSON contains operator (Op.jsonContains)
|
|
948
|
+
// PostgreSQL: @>, MySQL: JSON_CONTAINS, SQLite: json_extract
|
|
949
|
+
if (operator.$jsonContains !== undefined) {
|
|
950
|
+
const containsValue = operator.$jsonContains;
|
|
951
|
+
values.push(JSON.stringify(containsValue));
|
|
952
|
+
// Use dialect-specific JSON contains
|
|
953
|
+
// For cross-database compatibility, we use the default dialect approach
|
|
954
|
+
// PostgreSQL: @> operator
|
|
955
|
+
// MySQL: JSON_CONTAINS
|
|
956
|
+
// SQLite: json_each
|
|
957
|
+
if (this.options.dialectName === 'postgres') {
|
|
958
|
+
return `${this.escapeId(field)} @> ?`;
|
|
959
|
+
}
|
|
960
|
+
else if (this.options.dialectName === 'mysql' || this.options.dialectName === 'mariadb') {
|
|
961
|
+
return `JSON_CONTAINS(${this.escapeId(field)}, ?)`;
|
|
962
|
+
}
|
|
963
|
+
else {
|
|
964
|
+
// SQLite and default: use JSON comparison
|
|
965
|
+
return `${this.escapeId(field)} = ?`;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
// $jsonHasKey - JSON has key operator (Op.jsonHasKey)
|
|
969
|
+
// PostgreSQL: ? (jsonb_exists), MySQL: JSON_CONTAINS_PATH, SQLite: json_each
|
|
970
|
+
if (operator.$jsonHasKey !== undefined) {
|
|
971
|
+
const keyPath = String(operator.$jsonHasKey);
|
|
972
|
+
// Normalize the key path
|
|
973
|
+
const normalizedKey = keyPath.startsWith('$.') ? keyPath.substring(2) : keyPath;
|
|
974
|
+
if (this.options.dialectName === 'postgres') {
|
|
975
|
+
// PostgreSQL: ? operator for jsonb
|
|
976
|
+
return `${this.escapeId(field)} ? $1`;
|
|
977
|
+
}
|
|
978
|
+
else if (this.options.dialectName === 'mysql' || this.options.dialectName === 'mariadb') {
|
|
979
|
+
// MySQL/MariaDB: JSON_CONTAINS_PATH(col, 'one', '$.key')
|
|
980
|
+
return `JSON_CONTAINS_PATH(${this.escapeId(field)}, 'one', '$.${normalizedKey}')`;
|
|
981
|
+
}
|
|
982
|
+
else {
|
|
983
|
+
// SQLite: json_extract returns non-null if key exists
|
|
984
|
+
return `json_extract(${this.escapeId(field)}, '$.${normalizedKey}') IS NOT NULL`;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
// $jsonPath - JSON path query operator (Op.jsonPath)
|
|
988
|
+
// PostgreSQL: ->> and #>>, MySQL: JSON_EXTRACT, SQLite: json_extract
|
|
989
|
+
if (operator.$jsonPath !== undefined) {
|
|
990
|
+
const jsonPathValue = operator.$jsonPath;
|
|
991
|
+
if (typeof jsonPathValue === 'object' && jsonPathValue !== null && 'path' in jsonPathValue) {
|
|
992
|
+
const path = jsonPathValue.path;
|
|
993
|
+
const normalizedPath = path.startsWith('$.') ? path : `$.${path}`;
|
|
994
|
+
const compareValue = jsonPathValue.value;
|
|
995
|
+
if (compareValue !== undefined) {
|
|
996
|
+
// Value comparison: json_extract(field, '$.path') = ?
|
|
997
|
+
values.push(compareValue);
|
|
998
|
+
return `${this.buildJsonExtract(field, normalizedPath)} = ?`;
|
|
999
|
+
}
|
|
1000
|
+
// No value - just check if path exists (IS NOT NULL)
|
|
1001
|
+
return `${this.buildJsonExtract(field, normalizedPath)} IS NOT NULL`;
|
|
1002
|
+
}
|
|
1003
|
+
// Fallback
|
|
1004
|
+
return '';
|
|
1005
|
+
}
|
|
1006
|
+
// $jsonbExtract - JSONB extract field (PostgreSQL: ->)
|
|
1007
|
+
// Extracts a JSON object field, returns JSON
|
|
1008
|
+
if (operator.$jsonbExtract !== undefined) {
|
|
1009
|
+
const extractValue = operator.$jsonbExtract;
|
|
1010
|
+
if (typeof extractValue === 'object' && extractValue !== null && 'path' in extractValue) {
|
|
1011
|
+
const path = String(extractValue.path);
|
|
1012
|
+
const compareValue = extractValue.value;
|
|
1013
|
+
// PostgreSQL: field -> 'path'
|
|
1014
|
+
const columnRef = this.escapeId(field);
|
|
1015
|
+
if (this.options.dialectName === 'postgres' || this.options.dialectName === 'postgresql') {
|
|
1016
|
+
if (compareValue !== undefined) {
|
|
1017
|
+
values.push(compareValue);
|
|
1018
|
+
return `${columnRef} -> '${path}' = ?`;
|
|
1019
|
+
}
|
|
1020
|
+
return `${columnRef} -> '${path}' IS NOT NULL`;
|
|
1021
|
+
}
|
|
1022
|
+
// Fallback for other dialects: use json_extract
|
|
1023
|
+
const pathStr = path.startsWith('$.') ? path : `$.${path}`;
|
|
1024
|
+
if (compareValue !== undefined) {
|
|
1025
|
+
values.push(compareValue);
|
|
1026
|
+
return `json_extract(${columnRef}, '${pathStr}') = ?`;
|
|
1027
|
+
}
|
|
1028
|
+
return `json_extract(${columnRef}, '${pathStr}') IS NOT NULL`;
|
|
1029
|
+
}
|
|
1030
|
+
return '';
|
|
1031
|
+
}
|
|
1032
|
+
// $jsonbExtractText - JSONB extract field as text (PostgreSQL: ->>)
|
|
1033
|
+
// Extracts a JSON object field as text, returns text
|
|
1034
|
+
if (operator.$jsonbExtractText !== undefined) {
|
|
1035
|
+
const extractValue = operator.$jsonbExtractText;
|
|
1036
|
+
if (typeof extractValue === 'object' && extractValue !== null && 'path' in extractValue) {
|
|
1037
|
+
const path = String(extractValue.path);
|
|
1038
|
+
const compareValue = extractValue.value;
|
|
1039
|
+
// PostgreSQL: field ->> 'path'
|
|
1040
|
+
const columnRef = this.escapeId(field);
|
|
1041
|
+
if (this.options.dialectName === 'postgres' || this.options.dialectName === 'postgresql') {
|
|
1042
|
+
if (compareValue !== undefined) {
|
|
1043
|
+
values.push(compareValue);
|
|
1044
|
+
return `${columnRef} ->> '${path}' = ?`;
|
|
1045
|
+
}
|
|
1046
|
+
return `${columnRef} ->> '${path}' IS NOT NULL`;
|
|
1047
|
+
}
|
|
1048
|
+
// Fallback for other dialects: use json_extract
|
|
1049
|
+
const pathStr = path.startsWith('$.') ? path : `$.${path}`;
|
|
1050
|
+
if (compareValue !== undefined) {
|
|
1051
|
+
values.push(compareValue);
|
|
1052
|
+
return `json_extract(${columnRef}, '${pathStr}') = ?`;
|
|
1053
|
+
}
|
|
1054
|
+
return `json_extract(${columnRef}, '${pathStr}') IS NOT NULL`;
|
|
1055
|
+
}
|
|
1056
|
+
return '';
|
|
1057
|
+
}
|
|
1058
|
+
// $jsonbExtractPath - JSONB extract by path (PostgreSQL: #>)
|
|
1059
|
+
// Extracts JSON by path, returns JSON
|
|
1060
|
+
if (operator.$jsonbExtractPath !== undefined) {
|
|
1061
|
+
const extractValue = operator.$jsonbExtractPath;
|
|
1062
|
+
if (typeof extractValue === 'object' && extractValue !== null && 'path' in extractValue) {
|
|
1063
|
+
const pathValue = extractValue.path;
|
|
1064
|
+
const compareValue = extractValue.value;
|
|
1065
|
+
// Convert path to PostgreSQL format: ['a', 'b'] -> '{a, b}'
|
|
1066
|
+
const pgPath = Array.isArray(pathValue) ? `{${pathValue.join(', ')}}` : `{${pathValue}}`;
|
|
1067
|
+
const columnRef = this.escapeId(field);
|
|
1068
|
+
if (this.options.dialectName === 'postgres' || this.options.dialectName === 'postgresql') {
|
|
1069
|
+
if (compareValue !== undefined) {
|
|
1070
|
+
values.push(compareValue);
|
|
1071
|
+
return `${columnRef} #> '${pgPath}' = ?`;
|
|
1072
|
+
}
|
|
1073
|
+
return `${columnRef} #> '${pgPath}' IS NOT NULL`;
|
|
1074
|
+
}
|
|
1075
|
+
// Fallback for other dialects
|
|
1076
|
+
const pathStr = Array.isArray(pathValue) ? pathValue.join('.') : pathValue;
|
|
1077
|
+
const jsonPath = pathStr.startsWith('$.') ? pathStr : `$.${pathStr}`;
|
|
1078
|
+
if (compareValue !== undefined) {
|
|
1079
|
+
values.push(compareValue);
|
|
1080
|
+
return `json_extract(${columnRef}, '${jsonPath}') = ?`;
|
|
1081
|
+
}
|
|
1082
|
+
return `json_extract(${columnRef}, '${jsonPath}') IS NOT NULL`;
|
|
1083
|
+
}
|
|
1084
|
+
return '';
|
|
1085
|
+
}
|
|
1086
|
+
// $jsonbExtractPathText - JSONB extract by path as text (PostgreSQL: #>>)
|
|
1087
|
+
// Extracts JSON by path as text, returns text
|
|
1088
|
+
if (operator.$jsonbExtractPathText !== undefined) {
|
|
1089
|
+
const extractValue = operator.$jsonbExtractPathText;
|
|
1090
|
+
if (typeof extractValue === 'object' && extractValue !== null && 'path' in extractValue) {
|
|
1091
|
+
const pathValue = extractValue.path;
|
|
1092
|
+
const compareValue = extractValue.value;
|
|
1093
|
+
// Convert path to PostgreSQL format: ['a', 'b'] -> '{a, b}'
|
|
1094
|
+
const pgPath = Array.isArray(pathValue) ? `{${pathValue.join(', ')}}` : `{${pathValue}}`;
|
|
1095
|
+
const columnRef = this.escapeId(field);
|
|
1096
|
+
if (this.options.dialectName === 'postgres' || this.options.dialectName === 'postgresql') {
|
|
1097
|
+
if (compareValue !== undefined) {
|
|
1098
|
+
values.push(compareValue);
|
|
1099
|
+
return `${columnRef} #>> '${pgPath}' = ?`;
|
|
1100
|
+
}
|
|
1101
|
+
return `${columnRef} #>> '${pgPath}' IS NOT NULL`;
|
|
1102
|
+
}
|
|
1103
|
+
// Fallback for other dialects
|
|
1104
|
+
const pathStr = Array.isArray(pathValue) ? pathValue.join('.') : pathValue;
|
|
1105
|
+
const jsonPath = pathStr.startsWith('$.') ? pathStr : `$.${pathStr}`;
|
|
1106
|
+
if (compareValue !== undefined) {
|
|
1107
|
+
values.push(compareValue);
|
|
1108
|
+
return `json_extract(${columnRef}, '${jsonPath}') = ?`;
|
|
1109
|
+
}
|
|
1110
|
+
return `json_extract(${columnRef}, '${jsonPath}') IS NOT NULL`;
|
|
1111
|
+
}
|
|
1112
|
+
return '';
|
|
1113
|
+
}
|
|
1114
|
+
// $key - Get value at key path in JSON
|
|
1115
|
+
// Supports: { $key: 'address.city' } or { $key: { path: 'address.city', value: 'NYC' } }
|
|
1116
|
+
if (operator.$key !== undefined) {
|
|
1117
|
+
const keyValue = operator.$key;
|
|
1118
|
+
if (typeof keyValue === 'string') {
|
|
1119
|
+
// Just a key path, return the JSON extraction
|
|
1120
|
+
return `json_extract(${this.escapeId(field)}, '$.${keyValue}')`;
|
|
1121
|
+
}
|
|
1122
|
+
else if (typeof keyValue === 'object' && keyValue !== null && 'path' in keyValue) {
|
|
1123
|
+
// Key path with value comparison
|
|
1124
|
+
const keyObj = keyValue;
|
|
1125
|
+
const path = keyObj.path;
|
|
1126
|
+
const compareValue = keyObj.value;
|
|
1127
|
+
values.push(compareValue);
|
|
1128
|
+
return `json_extract(${this.escapeId(field)}, '$.${path}') = ?`;
|
|
1129
|
+
}
|
|
1130
|
+
// Fallback
|
|
1131
|
+
values.push(operator.$key);
|
|
1132
|
+
return `${this.escapeId(field)} = ?`;
|
|
1133
|
+
}
|
|
1134
|
+
// $jsonConcat - JSONB concatenation (PostgreSQL: ||)
|
|
1135
|
+
if (operator.$jsonConcat !== undefined) {
|
|
1136
|
+
const concatValue = operator.$jsonConcat;
|
|
1137
|
+
if (this.options.dialectName === 'postgres' || this.options.dialectName === 'postgresql') {
|
|
1138
|
+
values.push(JSON.stringify(concatValue));
|
|
1139
|
+
return `${this.escapeId(field)} || ?`;
|
|
1140
|
+
}
|
|
1141
|
+
// Fallback for other dialects - use string concatenation
|
|
1142
|
+
values.push(JSON.stringify(concatValue));
|
|
1143
|
+
return `${this.escapeId(field)} = ?`;
|
|
1144
|
+
}
|
|
1145
|
+
// $jsonDelete - JSONB delete key (PostgreSQL: -)
|
|
1146
|
+
if (operator.$jsonDelete !== undefined) {
|
|
1147
|
+
const deleteKey = operator.$jsonDelete;
|
|
1148
|
+
if (this.options.dialectName === 'postgres' || this.options.dialectName === 'postgresql') {
|
|
1149
|
+
return `${this.escapeId(field)} - '${deleteKey}'`;
|
|
1150
|
+
}
|
|
1151
|
+
// Fallback - not supported
|
|
1152
|
+
throw new Error('JSON key deletion is only supported in PostgreSQL');
|
|
1153
|
+
}
|
|
1154
|
+
// $jsonDeletePath - JSONB delete by path (PostgreSQL: #-)
|
|
1155
|
+
if (operator.$jsonDeletePath !== undefined) {
|
|
1156
|
+
const deletePath = operator.$jsonDeletePath;
|
|
1157
|
+
if (this.options.dialectName === 'postgres' || this.options.dialectName === 'postgresql') {
|
|
1158
|
+
const pgPath = Array.isArray(deletePath)
|
|
1159
|
+
? `{${deletePath.join(', ')}}`
|
|
1160
|
+
: `{${deletePath}}`;
|
|
1161
|
+
return `${this.escapeId(field)} #- '${pgPath}'`;
|
|
1162
|
+
}
|
|
1163
|
+
// Fallback - not supported
|
|
1164
|
+
throw new Error('JSON path deletion is only supported in PostgreSQL');
|
|
1165
|
+
}
|
|
1166
|
+
// $jsonPathExists - JSON path exists (PostgreSQL: @?)
|
|
1167
|
+
if (operator.$jsonPathExists !== undefined) {
|
|
1168
|
+
const pathValue = operator.$jsonPathExists;
|
|
1169
|
+
if (this.options.dialectName === 'postgres' || this.options.dialectName === 'postgresql') {
|
|
1170
|
+
return `${this.escapeId(field)} @? '${pathValue}'`;
|
|
1171
|
+
}
|
|
1172
|
+
// Fallback for other dialects - use JSON_EXISTS or similar
|
|
1173
|
+
return `${this.escapeId(field)} @? '${pathValue}'`;
|
|
1174
|
+
}
|
|
1175
|
+
// $jsonPathQuery - JSON path query (PostgreSQL: @@)
|
|
1176
|
+
if (operator.$jsonPathQuery !== undefined) {
|
|
1177
|
+
const pathValue = operator.$jsonPathQuery;
|
|
1178
|
+
if (this.options.dialectName === 'postgres' || this.options.dialectName === 'postgresql') {
|
|
1179
|
+
return `${this.escapeId(field)} @@ '${pathValue}'`;
|
|
1180
|
+
}
|
|
1181
|
+
// Fallback for other dialects
|
|
1182
|
+
return `${this.escapeId(field)} @@ '${pathValue}'`;
|
|
1183
|
+
}
|
|
1184
|
+
// $jsonTypeOf - JSON type of (PostgreSQL: json_typeof)
|
|
1185
|
+
if (operator.$jsonTypeOf !== undefined) {
|
|
1186
|
+
const typeValue = operator.$jsonTypeOf;
|
|
1187
|
+
if (this.options.dialectName === 'postgres' || this.options.dialectName === 'postgresql') {
|
|
1188
|
+
values.push(typeValue);
|
|
1189
|
+
return `json_typeof(${this.escapeId(field)}) = ?`;
|
|
1190
|
+
}
|
|
1191
|
+
// Fallback - not supported
|
|
1192
|
+
throw new Error('JSON type detection is only supported in PostgreSQL');
|
|
1193
|
+
}
|
|
1194
|
+
// $col - Column reference for comparisons (e.g., WHERE column1 = column2)
|
|
1195
|
+
if (operator.$col !== undefined) {
|
|
1196
|
+
const colName = String(operator.$col);
|
|
1197
|
+
return `${this.escapeId(field)} = ${this.escapeId(colName)}`;
|
|
1198
|
+
}
|
|
1199
|
+
// $match - Full-text search (MySQL MATCH AGAINST)
|
|
1200
|
+
// Format: { field: { $match: { columns: string[], mode: 'natural' | 'boolean', searchTerm: string } } }
|
|
1201
|
+
// or using Op.match(): { [Op.match(['title', 'body'])]: 'search term' }
|
|
1202
|
+
if (operator.$match !== undefined) {
|
|
1203
|
+
const matchValue = operator.$match;
|
|
1204
|
+
const searchTerm = typeof matchValue === 'string' ? matchValue : matchValue.searchTerm || '';
|
|
1205
|
+
const columns = matchValue.columns || [field];
|
|
1206
|
+
const mode = matchValue.mode || 'natural';
|
|
1207
|
+
// Helper to escape values using dialect
|
|
1208
|
+
const escapeValue = (val) => {
|
|
1209
|
+
if (this.options.dialect?.escape) {
|
|
1210
|
+
return this.options.dialect.escape(val);
|
|
1211
|
+
}
|
|
1212
|
+
if (typeof val === 'string') {
|
|
1213
|
+
return `'${val.replace(/'/g, "''")}'`;
|
|
1214
|
+
}
|
|
1215
|
+
return String(val);
|
|
1216
|
+
};
|
|
1217
|
+
// For MySQL dialect
|
|
1218
|
+
if (this.options.dialectName === 'mysql' || this.options.dialectName === 'mariadb') {
|
|
1219
|
+
const columnList = columns.map((c) => this.escapeId(c)).join(', ');
|
|
1220
|
+
const modeStr = mode === 'boolean' ? 'IN BOOLEAN MODE' : 'IN NATURAL LANGUAGE MODE';
|
|
1221
|
+
return `MATCH(${columnList}) AGAINST(${escapeValue(searchTerm)} ${modeStr})`;
|
|
1222
|
+
}
|
|
1223
|
+
// For PostgreSQL, use tsvector/tsquery
|
|
1224
|
+
if (this.options.dialectName === 'postgres') {
|
|
1225
|
+
const columnList = columns.map((c) => this.escapeId(c)).join(" || ' ' || ");
|
|
1226
|
+
values.push(searchTerm);
|
|
1227
|
+
return `to_tsvector('english', ${columnList}) @@ to_tsquery('english', ?)`;
|
|
1228
|
+
}
|
|
1229
|
+
// Default fallback for SQLite (no FTS support)
|
|
1230
|
+
values.push(searchTerm);
|
|
1231
|
+
return `${this.escapeId(field)} LIKE ?`;
|
|
1232
|
+
}
|
|
1233
|
+
// $tsvector - PostgreSQL to_tsvector
|
|
1234
|
+
if (operator.$tsvector !== undefined) {
|
|
1235
|
+
const tsvectorValue = operator.$tsvector;
|
|
1236
|
+
const column = tsvectorValue.column || field;
|
|
1237
|
+
const config = tsvectorValue.config || 'english';
|
|
1238
|
+
// This is typically used with @@ operator in PostgreSQL
|
|
1239
|
+
return `to_tsvector('${config}', ${this.escapeId(column)})`;
|
|
1240
|
+
}
|
|
1241
|
+
// $tsquery - PostgreSQL to_tsquery
|
|
1242
|
+
if (operator.$tsquery !== undefined) {
|
|
1243
|
+
const tsqueryValue = operator.$tsquery;
|
|
1244
|
+
const query = tsqueryValue.query;
|
|
1245
|
+
const config = tsqueryValue.config || 'english';
|
|
1246
|
+
if (query) {
|
|
1247
|
+
values.push(query);
|
|
1248
|
+
return `to_tsquery('${config}', ?)`;
|
|
1249
|
+
}
|
|
1250
|
+
// Handle case where tsquery is used with a value: { field: { $tsquery: 'term' } }
|
|
1251
|
+
return `to_tsquery('${config}', ?)`;
|
|
1252
|
+
}
|
|
1253
|
+
// $plainTsquery - PostgreSQL plainto_tsquery
|
|
1254
|
+
if (operator.$plainTsquery !== undefined) {
|
|
1255
|
+
const plainTsqueryValue = operator.$plainTsquery;
|
|
1256
|
+
const query = plainTsqueryValue.query;
|
|
1257
|
+
const config = plainTsqueryValue.config || 'english';
|
|
1258
|
+
if (query) {
|
|
1259
|
+
values.push(query);
|
|
1260
|
+
return `plainto_tsquery('${config}', ?)`;
|
|
1261
|
+
}
|
|
1262
|
+
return `plainto_tsquery('${config}', ?)`;
|
|
1263
|
+
}
|
|
1264
|
+
// $phraseTsquery - PostgreSQL phraseto_tsquery
|
|
1265
|
+
if (operator.$phraseTsquery !== undefined) {
|
|
1266
|
+
const phraseTsqueryValue = operator.$phraseTsquery;
|
|
1267
|
+
const query = phraseTsqueryValue.query;
|
|
1268
|
+
const config = phraseTsqueryValue.config || 'english';
|
|
1269
|
+
if (query) {
|
|
1270
|
+
values.push(query);
|
|
1271
|
+
return `phraseto_tsquery('${config}', ?)`;
|
|
1272
|
+
}
|
|
1273
|
+
return `phraseto_tsquery('${config}', ?)`;
|
|
1274
|
+
}
|
|
1275
|
+
// ============================================
|
|
1276
|
+
// Spatial/Geometric operators
|
|
1277
|
+
// ============================================
|
|
1278
|
+
// Helper to convert geometry value to SQL
|
|
1279
|
+
const buildGeometrySql = (geom, srid) => {
|
|
1280
|
+
if (typeof geom === 'string') {
|
|
1281
|
+
// WKT string - wrap with ST_GeomFromText
|
|
1282
|
+
const sridStr = srid ? `, ${srid}` : '';
|
|
1283
|
+
if (this.options.dialectName === 'mysql' || this.options.dialectName === 'mariadb') {
|
|
1284
|
+
return `ST_GeomFromText('${geom}'${sridStr})`;
|
|
1285
|
+
}
|
|
1286
|
+
return `ST_GeomFromText('${geom}'${sridStr})`;
|
|
1287
|
+
}
|
|
1288
|
+
if (typeof geom === 'object' && geom !== null && 'type' in geom && 'coordinates' in geom) {
|
|
1289
|
+
// GeoJSON object
|
|
1290
|
+
const sridStr = srid ? `, ${srid}` : '';
|
|
1291
|
+
return `ST_GeomFromGeoJSON('${JSON.stringify(geom)}'${sridStr})`;
|
|
1292
|
+
}
|
|
1293
|
+
// Just return as-is (assuming it's already a column reference)
|
|
1294
|
+
return String(geom);
|
|
1295
|
+
};
|
|
1296
|
+
// $stDistance - distance between two geometries
|
|
1297
|
+
if (operator.$stDistance !== undefined) {
|
|
1298
|
+
const distOptions = operator.$stDistance;
|
|
1299
|
+
const from = distOptions.from;
|
|
1300
|
+
const distance = distOptions.distance;
|
|
1301
|
+
const srid = distOptions.srid || 4326;
|
|
1302
|
+
const fromSql = buildGeometrySql(from, srid);
|
|
1303
|
+
const fieldGeom = this.escapeId(field);
|
|
1304
|
+
if (this.options.dialectName === 'mysql' || this.options.dialectName === 'mariadb') {
|
|
1305
|
+
// MySQL: ST_Distance returns distance in units of the SRID
|
|
1306
|
+
values.push(distance);
|
|
1307
|
+
return `ST_Distance(${fieldGeom}, ST_GeomFromText('${typeof from === 'string' ? from : ''}', ${srid})) < ?`;
|
|
1308
|
+
}
|
|
1309
|
+
// PostgreSQL: ST_Distance for geometry, ST_Distance for geography (in meters)
|
|
1310
|
+
values.push(distance);
|
|
1311
|
+
return `ST_Distance(${fieldGeom}, ${fromSql}) < ?`;
|
|
1312
|
+
}
|
|
1313
|
+
// $stWithin - geometry A is within geometry B
|
|
1314
|
+
if (operator.$stWithin !== undefined) {
|
|
1315
|
+
const withinOptions = operator.$stWithin;
|
|
1316
|
+
const geometry = withinOptions.geometry;
|
|
1317
|
+
const srid = withinOptions.srid || 4326;
|
|
1318
|
+
const geomSql = buildGeometrySql(geometry, srid);
|
|
1319
|
+
const fieldGeom = this.escapeId(field);
|
|
1320
|
+
if (this.options.dialectName === 'mysql' || this.options.dialectName === 'mariadb') {
|
|
1321
|
+
return `ST_Within(${fieldGeom}, ${geomSql})`;
|
|
1322
|
+
}
|
|
1323
|
+
// PostgreSQL
|
|
1324
|
+
return `ST_Within(${fieldGeom}, ${geomSql})`;
|
|
1325
|
+
}
|
|
1326
|
+
// $stContains - geometry A contains geometry B
|
|
1327
|
+
if (operator.$stContains !== undefined) {
|
|
1328
|
+
const containsOptions = operator.$stContains;
|
|
1329
|
+
const geometry = containsOptions.geometry;
|
|
1330
|
+
const srid = containsOptions.srid || 4326;
|
|
1331
|
+
const geomSql = buildGeometrySql(geometry, srid);
|
|
1332
|
+
const fieldGeom = this.escapeId(field);
|
|
1333
|
+
if (this.options.dialectName === 'mysql' || this.options.dialectName === 'mariadb') {
|
|
1334
|
+
return `ST_Contains(${fieldGeom}, ${geomSql})`;
|
|
1335
|
+
}
|
|
1336
|
+
// PostgreSQL
|
|
1337
|
+
return `ST_Contains(${fieldGeom}, ${geomSql})`;
|
|
1338
|
+
}
|
|
1339
|
+
// $stIntersects - two geometries intersect
|
|
1340
|
+
if (operator.$stIntersects !== undefined) {
|
|
1341
|
+
const intersectsOptions = operator.$stIntersects;
|
|
1342
|
+
const geometry = intersectsOptions.geometry;
|
|
1343
|
+
const srid = intersectsOptions.srid || 4326;
|
|
1344
|
+
const geomSql = buildGeometrySql(geometry, srid);
|
|
1345
|
+
const fieldGeom = this.escapeId(field);
|
|
1346
|
+
if (this.options.dialectName === 'mysql' || this.options.dialectName === 'mariadb') {
|
|
1347
|
+
return `ST_Intersects(${fieldGeom}, ${geomSql})`;
|
|
1348
|
+
}
|
|
1349
|
+
// PostgreSQL
|
|
1350
|
+
return `ST_Intersects(${fieldGeom}, ${geomSql})`;
|
|
1351
|
+
}
|
|
1352
|
+
// $stDWithin - geometries are within a given distance
|
|
1353
|
+
if (operator.$stDWithin !== undefined) {
|
|
1354
|
+
const dWithinOptions = operator.$stDWithin;
|
|
1355
|
+
const from = dWithinOptions.from;
|
|
1356
|
+
const distance = dWithinOptions.distance;
|
|
1357
|
+
const srid = dWithinOptions.srid || 4326;
|
|
1358
|
+
const spheroid = dWithinOptions.spheroid !== false;
|
|
1359
|
+
const fromSql = buildGeometrySql(from, srid);
|
|
1360
|
+
const fieldGeom = this.escapeId(field);
|
|
1361
|
+
if (this.options.dialectName === 'mysql' || this.options.dialectName === 'mariadb') {
|
|
1362
|
+
// MySQL 8.0+ uses ST_DWithin
|
|
1363
|
+
values.push(distance);
|
|
1364
|
+
return `ST_DWithin(${fieldGeom}, ST_GeomFromText('${typeof from === 'string' ? from : ''}', ${srid}), ?)`;
|
|
1365
|
+
}
|
|
1366
|
+
// PostgreSQL: ST_DWithin for geometry, ST_DWithin for geography
|
|
1367
|
+
if (spheroid) {
|
|
1368
|
+
values.push(distance);
|
|
1369
|
+
return `ST_DWithin(${fieldGeom}, ${fromSql}, ?)`;
|
|
1370
|
+
}
|
|
1371
|
+
values.push(distance);
|
|
1372
|
+
return `ST_DWithin(${fieldGeom}, ${fromSql}, ?)`;
|
|
1373
|
+
}
|
|
1374
|
+
// $stCrosses - two geometries cross
|
|
1375
|
+
if (operator.$stCrosses !== undefined) {
|
|
1376
|
+
const crossesOptions = operator.$stCrosses;
|
|
1377
|
+
const geometry = crossesOptions.geometry;
|
|
1378
|
+
const srid = crossesOptions.srid || 4326;
|
|
1379
|
+
const geomSql = buildGeometrySql(geometry, srid);
|
|
1380
|
+
const fieldGeom = this.escapeId(field);
|
|
1381
|
+
if (this.options.dialectName === 'mysql' || this.options.dialectName === 'mariadb') {
|
|
1382
|
+
return `ST_Crosses(${fieldGeom}, ${geomSql})`;
|
|
1383
|
+
}
|
|
1384
|
+
return `ST_Crosses(${fieldGeom}, ${geomSql})`;
|
|
1385
|
+
}
|
|
1386
|
+
// $stOverlaps - two geometries overlap
|
|
1387
|
+
if (operator.$stOverlaps !== undefined) {
|
|
1388
|
+
const overlapsOptions = operator.$stOverlaps;
|
|
1389
|
+
const geometry = overlapsOptions.geometry;
|
|
1390
|
+
const srid = overlapsOptions.srid || 4326;
|
|
1391
|
+
const geomSql = buildGeometrySql(geometry, srid);
|
|
1392
|
+
const fieldGeom = this.escapeId(field);
|
|
1393
|
+
if (this.options.dialectName === 'mysql' || this.options.dialectName === 'mariadb') {
|
|
1394
|
+
return `ST_Overlaps(${fieldGeom}, ${geomSql})`;
|
|
1395
|
+
}
|
|
1396
|
+
return `ST_Overlaps(${fieldGeom}, ${geomSql})`;
|
|
1397
|
+
}
|
|
1398
|
+
// $stTouches - two geometries touch
|
|
1399
|
+
if (operator.$stTouches !== undefined) {
|
|
1400
|
+
const touchesOptions = operator.$stTouches;
|
|
1401
|
+
const geometry = touchesOptions.geometry;
|
|
1402
|
+
const srid = touchesOptions.srid || 4326;
|
|
1403
|
+
const geomSql = buildGeometrySql(geometry, srid);
|
|
1404
|
+
const fieldGeom = this.escapeId(field);
|
|
1405
|
+
if (this.options.dialectName === 'mysql' || this.options.dialectName === 'mariadb') {
|
|
1406
|
+
return `ST_Touches(${fieldGeom}, ${geomSql})`;
|
|
1407
|
+
}
|
|
1408
|
+
return `ST_Touches(${fieldGeom}, ${geomSql})`;
|
|
1409
|
+
}
|
|
1410
|
+
// $stEquals - two geometries are equal
|
|
1411
|
+
if (operator.$stEquals !== undefined) {
|
|
1412
|
+
const equalsOptions = operator.$stEquals;
|
|
1413
|
+
const geometry = equalsOptions.geometry;
|
|
1414
|
+
const srid = equalsOptions.srid || 4326;
|
|
1415
|
+
const geomSql = buildGeometrySql(geometry, srid);
|
|
1416
|
+
const fieldGeom = this.escapeId(field);
|
|
1417
|
+
if (this.options.dialectName === 'mysql' || this.options.dialectName === 'mariadb') {
|
|
1418
|
+
return `ST_Equals(${fieldGeom}, ${geomSql})`;
|
|
1419
|
+
}
|
|
1420
|
+
return `ST_Equals(${fieldGeom}, ${geomSql})`;
|
|
1421
|
+
}
|
|
1422
|
+
// $stIsValid - check if geometry is valid
|
|
1423
|
+
if (operator.$stIsValid !== undefined) {
|
|
1424
|
+
const fieldGeom = this.escapeId(field);
|
|
1425
|
+
if (this.options.dialectName === 'mysql' || this.options.dialectName === 'mariadb') {
|
|
1426
|
+
return `ST_IsValid(${fieldGeom})`;
|
|
1427
|
+
}
|
|
1428
|
+
return `ST_IsValid(${fieldGeom})`;
|
|
1429
|
+
}
|
|
1430
|
+
// Handle nested object as implicit equality
|
|
1431
|
+
values.push(operator);
|
|
1432
|
+
return `${this.escapeId(field)} = ?`;
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Escape an identifier (column/table name)
|
|
1436
|
+
*/
|
|
1437
|
+
escapeId(identifier) {
|
|
1438
|
+
const id = String(identifier ?? '');
|
|
1439
|
+
if (this.options.dialect?.escapeId) {
|
|
1440
|
+
return this.options.dialect.escapeId(id);
|
|
1441
|
+
}
|
|
1442
|
+
// Default quoting
|
|
1443
|
+
return `"${id.replace(/"/g, '""')}"`;
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Escape a value for use in a query
|
|
1447
|
+
*/
|
|
1448
|
+
escapeValue(value) {
|
|
1449
|
+
if (value === null) {
|
|
1450
|
+
return 'NULL';
|
|
1451
|
+
}
|
|
1452
|
+
if (typeof value === 'string') {
|
|
1453
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
1454
|
+
}
|
|
1455
|
+
if (typeof value === 'number') {
|
|
1456
|
+
return String(value);
|
|
1457
|
+
}
|
|
1458
|
+
if (typeof value === 'boolean') {
|
|
1459
|
+
return value ? 'TRUE' : 'FALSE';
|
|
1460
|
+
}
|
|
1461
|
+
if (value instanceof Date) {
|
|
1462
|
+
return `'${value.toISOString()}'`;
|
|
1463
|
+
}
|
|
1464
|
+
// Default: JSON stringify
|
|
1465
|
+
return `'${JSON.stringify(value).replace(/'/g, "''")}'`;
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* Build a PostgreSQL array literal from an array value
|
|
1469
|
+
* e.g., ['a', 'b'] => ARRAY['a', 'b']
|
|
1470
|
+
*/
|
|
1471
|
+
buildArrayLiteral(value) {
|
|
1472
|
+
if (!Array.isArray(value)) {
|
|
1473
|
+
// Single value - wrap in array
|
|
1474
|
+
const escaped = this.options.dialect?.escape
|
|
1475
|
+
? this.options.dialect.escape(value)
|
|
1476
|
+
: this.escapeValue(value);
|
|
1477
|
+
return `ARRAY[${escaped}]`;
|
|
1478
|
+
}
|
|
1479
|
+
if (value.length === 0) {
|
|
1480
|
+
return 'ARRAY[]';
|
|
1481
|
+
}
|
|
1482
|
+
const escapedValues = value.map((v) => {
|
|
1483
|
+
return this.options.dialect?.escape ? this.options.dialect.escape(v) : this.escapeValue(v);
|
|
1484
|
+
});
|
|
1485
|
+
return `ARRAY[${escapedValues.join(', ')}]`;
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Build a JSON extraction expression based on the dialect
|
|
1489
|
+
* SQLite/MySQL: json_extract(column, '$.path')
|
|
1490
|
+
* PostgreSQL: column->>'path' or #>>'{path}'
|
|
1491
|
+
*
|
|
1492
|
+
* @param field - The JSON/JSONB column name
|
|
1493
|
+
* @param path - The JSON path to extract (e.g., 'settings.theme' or '$.settings.theme')
|
|
1494
|
+
* @returns SQL fragment for JSON extraction
|
|
1495
|
+
*/
|
|
1496
|
+
buildJsonExtract(field, path) {
|
|
1497
|
+
const columnRef = this.escapeId(field);
|
|
1498
|
+
const normalizedPath = path.startsWith('$.') ? path : `$.${path}`;
|
|
1499
|
+
// Get dialect name from options if available
|
|
1500
|
+
const dialectName = this.options?.dialectName;
|
|
1501
|
+
// PostgreSQL uses different syntax
|
|
1502
|
+
if (dialectName === 'postgres' || dialectName === 'postgresql') {
|
|
1503
|
+
// Convert path to PostgreSQL format: $.settings.theme -> 'settings', 'theme'
|
|
1504
|
+
const pathParts = normalizedPath.substring(2).split('.');
|
|
1505
|
+
const pgPath = pathParts.map((p) => `'${p}'`).join(', ');
|
|
1506
|
+
return `${columnRef} #>> '{${pgPath}}'`;
|
|
1507
|
+
}
|
|
1508
|
+
// SQLite and MySQL use JSON_EXTRACT / json_extract
|
|
1509
|
+
return `json_extract(${columnRef}, '${normalizedPath}')`;
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Build a JSON path query - alias for buildJsonExtract for compatibility
|
|
1513
|
+
*/
|
|
1514
|
+
jsonExtract(field, path) {
|
|
1515
|
+
return this.buildJsonExtract(field, path);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
exports.WhereBuilder = WhereBuilder;
|
|
1519
|
+
/**
|
|
1520
|
+
* Create a WhereBuilder instance with default options
|
|
1521
|
+
*/
|
|
1522
|
+
function createWhereBuilder(options) {
|
|
1523
|
+
return new WhereBuilder(options);
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Quick helper to build a WHERE clause
|
|
1527
|
+
*/
|
|
1528
|
+
function buildWhereClause(where, options) {
|
|
1529
|
+
const builder = new WhereBuilder(options);
|
|
1530
|
+
return builder.build(where);
|
|
1531
|
+
}
|
|
1532
|
+
exports.default = WhereBuilder;
|