local-lite 0.0.4 → 0.0.6
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/dist/adapters/local/better-sqlite3.d.ts +4 -1
- package/dist/adapters/local/better-sqlite3.js +34 -14
- package/dist/adapters/local/expo-sqlite.d.ts +10 -2
- package/dist/adapters/local/expo-sqlite.js +36 -20
- package/dist/adapters/local/node-sqlite.d.ts +4 -1
- package/dist/adapters/local/node-sqlite.js +29 -12
- package/dist/adapters/remote/firestore.js +39 -32
- package/dist/adapters/remote/supabase.d.ts +7 -2
- package/dist/adapters/remote/supabase.js +192 -85
- package/dist/config/types.d.ts +1 -1
- package/dist/core/client.d.ts +52 -15
- package/dist/core/client.js +195 -208
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/outbox-store.d.ts +5 -0
- package/dist/core/outbox-store.js +29 -4
- package/dist/core/query-builder.d.ts +120 -0
- package/dist/core/query-builder.js +441 -0
- package/dist/core/query.js +28 -32
- package/dist/core/schema-manager.d.ts +4 -15
- package/dist/core/schema-manager.js +48 -103
- package/dist/core/sync-worker.js +7 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/react/SyncInspector.js +0 -70
- package/dist/react/index.d.ts +39 -0
- package/dist/react/index.js +232 -34
- package/dist/react/status.d.ts +5 -0
- package/dist/react/status.js +20 -12
- package/dist/schema/generator.d.ts +49 -0
- package/dist/schema/generator.js +209 -0
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +17 -0
- package/dist/utils/uuid.d.ts +1 -1
- package/dist/utils/uuid.js +7 -16
- package/package.json +7 -2
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { LocalFirstClient, TransactionalClient } from './client.js';
|
|
2
|
+
import { QueryOptions, WhereFilter } from './types.js';
|
|
3
|
+
type ClientLike = LocalFirstClient | TransactionalClient;
|
|
4
|
+
/**
|
|
5
|
+
* Escapes an identifier or compound identifier (e.g. 'table.column' or 'table.col as alias').
|
|
6
|
+
*/
|
|
7
|
+
export declare function formatColumnIdentifier(col: string): string;
|
|
8
|
+
export interface JoinDefinition {
|
|
9
|
+
type: 'INNER' | 'LEFT' | 'CROSS';
|
|
10
|
+
table: string;
|
|
11
|
+
firstCol: string;
|
|
12
|
+
secondCol: string;
|
|
13
|
+
}
|
|
14
|
+
export interface AggregateSpec {
|
|
15
|
+
where?: WhereFilter;
|
|
16
|
+
count?: boolean;
|
|
17
|
+
sum?: string[];
|
|
18
|
+
avg?: string[];
|
|
19
|
+
min?: string[];
|
|
20
|
+
max?: string[];
|
|
21
|
+
}
|
|
22
|
+
export interface AggregateResult {
|
|
23
|
+
count?: number;
|
|
24
|
+
sum?: Record<string, number>;
|
|
25
|
+
avg?: Record<string, number | null>;
|
|
26
|
+
min?: Record<string, any>;
|
|
27
|
+
max?: Record<string, any>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Fluent, type-safe SQL Query Builder for local-first SQLite operations.
|
|
31
|
+
* Allows writing expressive JavaScript code that compiles and executes high-performance SQL under the hood.
|
|
32
|
+
*/
|
|
33
|
+
export declare class QueryBuilder<T = any> {
|
|
34
|
+
private client;
|
|
35
|
+
private tableName;
|
|
36
|
+
private selectCols;
|
|
37
|
+
private joins;
|
|
38
|
+
private whereConditions;
|
|
39
|
+
private orderColumn?;
|
|
40
|
+
private orderDirection;
|
|
41
|
+
private limitCount?;
|
|
42
|
+
private offsetCount?;
|
|
43
|
+
private groupByCols;
|
|
44
|
+
private includeDeleted;
|
|
45
|
+
constructor(client: ClientLike, tableName: string);
|
|
46
|
+
/**
|
|
47
|
+
* Specifies which columns to select.
|
|
48
|
+
* Supports column aliases (e.g. ['id', 'contracts.rent_amount AS rent', 'houses.house_number']).
|
|
49
|
+
*/
|
|
50
|
+
select(...columns: Array<string | string[]>): this;
|
|
51
|
+
/**
|
|
52
|
+
* Inner joins another SQLite table on a matching column expression.
|
|
53
|
+
*/
|
|
54
|
+
join(table: string, firstCol: string, secondCol: string): this;
|
|
55
|
+
/**
|
|
56
|
+
* Left joins another SQLite table on a matching column expression.
|
|
57
|
+
*/
|
|
58
|
+
leftJoin(table: string, firstCol: string, secondCol: string): this;
|
|
59
|
+
/**
|
|
60
|
+
* Adds WHERE criteria to filter results.
|
|
61
|
+
* Supports:
|
|
62
|
+
* - where({ status: 'ACTIVE' })
|
|
63
|
+
* - where('status', 'ACTIVE')
|
|
64
|
+
* - where('end_date', '>=', today)
|
|
65
|
+
*/
|
|
66
|
+
where(columnOrFilter: string | WhereFilter, operatorOrValue?: any, value?: any): this;
|
|
67
|
+
/**
|
|
68
|
+
* Orders query results by column and direction.
|
|
69
|
+
*/
|
|
70
|
+
orderBy(column: string, direction?: 'asc' | 'desc' | 'ASC' | 'DESC'): this;
|
|
71
|
+
/**
|
|
72
|
+
* Groups rows by one or more columns for aggregation.
|
|
73
|
+
*/
|
|
74
|
+
groupBy(...columns: Array<string | string[]>): this;
|
|
75
|
+
/**
|
|
76
|
+
* Limits the number of returned records.
|
|
77
|
+
*/
|
|
78
|
+
limit(count: number): this;
|
|
79
|
+
/**
|
|
80
|
+
* Offsets the starting record position.
|
|
81
|
+
*/
|
|
82
|
+
offset(count: number): this;
|
|
83
|
+
/**
|
|
84
|
+
* Includes soft-deleted rows in results.
|
|
85
|
+
*/
|
|
86
|
+
withDeleted(include?: boolean): this;
|
|
87
|
+
/**
|
|
88
|
+
* Applies standard QueryOptions (select, where, orderBy, limit, offset, includeDeleted) to this query.
|
|
89
|
+
*/
|
|
90
|
+
applyOptions(options?: QueryOptions): this;
|
|
91
|
+
/**
|
|
92
|
+
* Compiles the current builder state into an executable SQL string and parameter list.
|
|
93
|
+
* Uses module-scoped compileComparison / compileWhereConditions helpers (defined once).
|
|
94
|
+
*/
|
|
95
|
+
toSQL(): {
|
|
96
|
+
sql: string;
|
|
97
|
+
params: any[];
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Executes the compiled SQL query against SQLite and returns rows.
|
|
101
|
+
*/
|
|
102
|
+
execute(): Promise<T[]>;
|
|
103
|
+
/**
|
|
104
|
+
* Returns the first matching record or null if not found.
|
|
105
|
+
*/
|
|
106
|
+
first(): Promise<T | null>;
|
|
107
|
+
/**
|
|
108
|
+
* Executes a COUNT(*) in SQLite and returns the numeric count directly.
|
|
109
|
+
*/
|
|
110
|
+
count(): Promise<number>;
|
|
111
|
+
/**
|
|
112
|
+
* Executes a SUM(column) in SQLite and returns the numeric total directly.
|
|
113
|
+
*/
|
|
114
|
+
sum(column: string): Promise<number>;
|
|
115
|
+
/**
|
|
116
|
+
* Executes an AVG(column) in SQLite and returns the average directly.
|
|
117
|
+
*/
|
|
118
|
+
avg(column: string): Promise<number | null>;
|
|
119
|
+
}
|
|
120
|
+
export {};
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QueryBuilder = void 0;
|
|
4
|
+
exports.formatColumnIdentifier = formatColumnIdentifier;
|
|
5
|
+
/**
|
|
6
|
+
* Escapes an identifier or compound identifier (e.g. 'table.column' or 'table.col as alias').
|
|
7
|
+
*/
|
|
8
|
+
function formatColumnIdentifier(col) {
|
|
9
|
+
const trimmed = col.trim();
|
|
10
|
+
if (trimmed === '*')
|
|
11
|
+
return '*';
|
|
12
|
+
// Handle "expression AS alias"
|
|
13
|
+
const asMatch = trimmed.match(/^(.+?)\s+(?:AS|as)\s+([A-Za-z0-9_"]+)$/i);
|
|
14
|
+
if (asMatch) {
|
|
15
|
+
const [, expr, rawAlias] = asMatch;
|
|
16
|
+
const unquotedAlias = rawAlias.startsWith('"') && rawAlias.endsWith('"')
|
|
17
|
+
? rawAlias.slice(1, -1)
|
|
18
|
+
: rawAlias;
|
|
19
|
+
const cleanAlias = `"${unquotedAlias.replace(/"/g, '""')}"`;
|
|
20
|
+
if (expr.includes('(') && expr.endsWith(')')) {
|
|
21
|
+
return `${expr} AS ${cleanAlias}`;
|
|
22
|
+
}
|
|
23
|
+
return `${formatColumnIdentifier(expr)} AS ${cleanAlias}`;
|
|
24
|
+
}
|
|
25
|
+
// Handle function calls without alias (e.g. COUNT(*))
|
|
26
|
+
if (trimmed.includes('(') && trimmed.endsWith(')')) {
|
|
27
|
+
return trimmed;
|
|
28
|
+
}
|
|
29
|
+
// Handle "table.column"
|
|
30
|
+
if (trimmed.includes('.')) {
|
|
31
|
+
return trimmed
|
|
32
|
+
.split('.')
|
|
33
|
+
.map((part) => `"${part.replace(/"/g, '""')}"`)
|
|
34
|
+
.join('.');
|
|
35
|
+
}
|
|
36
|
+
return `"${trimmed.replace(/"/g, '""')}"`;
|
|
37
|
+
}
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Module-scoped SQL compilation helpers (defined once, reused on every query)
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
/**
|
|
42
|
+
* Compiles a single column comparison value into one or more SQL fragments.
|
|
43
|
+
* Supports null, boolean, scalar, and operator-object forms ($eq, $gt, $in, …).
|
|
44
|
+
*/
|
|
45
|
+
function compileComparison(safeCol, val, params) {
|
|
46
|
+
if (val === null) {
|
|
47
|
+
return `${safeCol} IS NULL`;
|
|
48
|
+
}
|
|
49
|
+
if (typeof val === 'boolean') {
|
|
50
|
+
params.push(val ? 1 : 0);
|
|
51
|
+
return `${safeCol} = ?`;
|
|
52
|
+
}
|
|
53
|
+
if (typeof val === 'string' || typeof val === 'number') {
|
|
54
|
+
params.push(val);
|
|
55
|
+
return `${safeCol} = ?`;
|
|
56
|
+
}
|
|
57
|
+
if (typeof val === 'object' && !Array.isArray(val)) {
|
|
58
|
+
const ops = val;
|
|
59
|
+
const subClauses = [];
|
|
60
|
+
const eqVal = ops.$eq !== undefined ? ops.$eq : ops.eq;
|
|
61
|
+
if (eqVal !== undefined) {
|
|
62
|
+
subClauses.push(`${safeCol} = ?`);
|
|
63
|
+
params.push(typeof eqVal === 'boolean' ? (eqVal ? 1 : 0) : eqVal);
|
|
64
|
+
}
|
|
65
|
+
const neVal = ops.$ne !== undefined ? ops.$ne : ops.ne;
|
|
66
|
+
if (neVal !== undefined) {
|
|
67
|
+
subClauses.push(`${safeCol} != ?`);
|
|
68
|
+
params.push(typeof neVal === 'boolean' ? (neVal ? 1 : 0) : neVal);
|
|
69
|
+
}
|
|
70
|
+
const gtVal = ops.$gt !== undefined ? ops.$gt : ops.gt;
|
|
71
|
+
if (gtVal !== undefined) {
|
|
72
|
+
subClauses.push(`${safeCol} > ?`);
|
|
73
|
+
params.push(gtVal);
|
|
74
|
+
}
|
|
75
|
+
const gteVal = ops.$gte !== undefined ? ops.$gte : ops.gte;
|
|
76
|
+
if (gteVal !== undefined) {
|
|
77
|
+
subClauses.push(`${safeCol} >= ?`);
|
|
78
|
+
params.push(gteVal);
|
|
79
|
+
}
|
|
80
|
+
const ltVal = ops.$lt !== undefined ? ops.$lt : ops.lt;
|
|
81
|
+
if (ltVal !== undefined) {
|
|
82
|
+
subClauses.push(`${safeCol} < ?`);
|
|
83
|
+
params.push(ltVal);
|
|
84
|
+
}
|
|
85
|
+
const lteVal = ops.$lte !== undefined ? ops.$lte : ops.lte;
|
|
86
|
+
if (lteVal !== undefined) {
|
|
87
|
+
subClauses.push(`${safeCol} <= ?`);
|
|
88
|
+
params.push(lteVal);
|
|
89
|
+
}
|
|
90
|
+
const inVal = ops.$in !== undefined ? ops.$in : ops.in;
|
|
91
|
+
if (Array.isArray(inVal) && inVal.length > 0) {
|
|
92
|
+
const placeholders = inVal.map(() => '?').join(', ');
|
|
93
|
+
subClauses.push(`${safeCol} IN (${placeholders})`);
|
|
94
|
+
params.push(...inVal);
|
|
95
|
+
}
|
|
96
|
+
const ninVal = ops.$nin !== undefined ? ops.$nin : ops.nin;
|
|
97
|
+
if (Array.isArray(ninVal) && ninVal.length > 0) {
|
|
98
|
+
const placeholders = ninVal.map(() => '?').join(', ');
|
|
99
|
+
subClauses.push(`${safeCol} NOT IN (${placeholders})`);
|
|
100
|
+
params.push(...ninVal);
|
|
101
|
+
}
|
|
102
|
+
const containsVal = ops.$contains !== undefined ? ops.$contains : ops.contains;
|
|
103
|
+
if (containsVal !== undefined) {
|
|
104
|
+
subClauses.push(`${safeCol} LIKE ?`);
|
|
105
|
+
params.push(`%${containsVal}%`);
|
|
106
|
+
}
|
|
107
|
+
const startsWithVal = ops.$startsWith !== undefined ? ops.$startsWith : ops.startsWith;
|
|
108
|
+
if (startsWithVal !== undefined) {
|
|
109
|
+
subClauses.push(`${safeCol} LIKE ?`);
|
|
110
|
+
params.push(`${startsWithVal}%`);
|
|
111
|
+
}
|
|
112
|
+
const endsWithVal = ops.$endsWith !== undefined ? ops.$endsWith : ops.endsWith;
|
|
113
|
+
if (endsWithVal !== undefined) {
|
|
114
|
+
subClauses.push(`${safeCol} LIKE ?`);
|
|
115
|
+
params.push(`%${endsWithVal}`);
|
|
116
|
+
}
|
|
117
|
+
const nullVal = ops.$null !== undefined ? ops.$null : ops.null;
|
|
118
|
+
if (nullVal !== undefined) {
|
|
119
|
+
subClauses.push(nullVal ? `${safeCol} IS NULL` : `${safeCol} IS NOT NULL`);
|
|
120
|
+
}
|
|
121
|
+
if (subClauses.length > 0) {
|
|
122
|
+
return subClauses.join(' AND ');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Recursively compiles a where-conditions object (including $or / $and) into SQL clauses.
|
|
129
|
+
*/
|
|
130
|
+
function compileWhereConditions(where, params) {
|
|
131
|
+
const clauses = [];
|
|
132
|
+
// Handle $or — compile each branch, join with OR
|
|
133
|
+
if (Array.isArray(where.$or) && where.$or.length > 0) {
|
|
134
|
+
const orClauses = [];
|
|
135
|
+
for (const sub of where.$or) {
|
|
136
|
+
if (typeof sub === 'object' && sub !== null) {
|
|
137
|
+
const subList = compileWhereConditions(sub, params);
|
|
138
|
+
if (subList.length > 0) {
|
|
139
|
+
orClauses.push(`(${subList.join(' AND ')})`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (orClauses.length > 0) {
|
|
144
|
+
clauses.push(`(${orClauses.join(' OR ')})`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Handle $and — compile each branch, join with AND
|
|
148
|
+
if (Array.isArray(where.$and) && where.$and.length > 0) {
|
|
149
|
+
const andClauses = [];
|
|
150
|
+
for (const sub of where.$and) {
|
|
151
|
+
if (typeof sub === 'object' && sub !== null) {
|
|
152
|
+
const subList = compileWhereConditions(sub, params);
|
|
153
|
+
if (subList.length > 0) {
|
|
154
|
+
andClauses.push(`(${subList.join(' AND ')})`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (andClauses.length > 0) {
|
|
159
|
+
clauses.push(`(${andClauses.join(' AND ')})`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Handle regular column conditions
|
|
163
|
+
for (const [col, val] of Object.entries(where)) {
|
|
164
|
+
if (col === '$or' || col === '$and')
|
|
165
|
+
continue;
|
|
166
|
+
const safeCol = formatColumnIdentifier(col);
|
|
167
|
+
const clause = compileComparison(safeCol, val, params);
|
|
168
|
+
if (clause) {
|
|
169
|
+
clauses.push(clause);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return clauses;
|
|
173
|
+
}
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
/**
|
|
176
|
+
* Fluent, type-safe SQL Query Builder for local-first SQLite operations.
|
|
177
|
+
* Allows writing expressive JavaScript code that compiles and executes high-performance SQL under the hood.
|
|
178
|
+
*/
|
|
179
|
+
class QueryBuilder {
|
|
180
|
+
client;
|
|
181
|
+
tableName;
|
|
182
|
+
selectCols = ['*'];
|
|
183
|
+
joins = [];
|
|
184
|
+
whereConditions = {};
|
|
185
|
+
orderColumn;
|
|
186
|
+
orderDirection = 'ASC';
|
|
187
|
+
limitCount;
|
|
188
|
+
offsetCount;
|
|
189
|
+
groupByCols = [];
|
|
190
|
+
includeDeleted = false;
|
|
191
|
+
constructor(client, tableName) {
|
|
192
|
+
this.client = client;
|
|
193
|
+
this.tableName = tableName;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Specifies which columns to select.
|
|
197
|
+
* Supports column aliases (e.g. ['id', 'contracts.rent_amount AS rent', 'houses.house_number']).
|
|
198
|
+
*/
|
|
199
|
+
select(...columns) {
|
|
200
|
+
const flat = columns.flat();
|
|
201
|
+
if (flat.length > 0) {
|
|
202
|
+
this.selectCols = flat;
|
|
203
|
+
}
|
|
204
|
+
return this;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Inner joins another SQLite table on a matching column expression.
|
|
208
|
+
*/
|
|
209
|
+
join(table, firstCol, secondCol) {
|
|
210
|
+
this.joins.push({ type: 'INNER', table, firstCol, secondCol });
|
|
211
|
+
return this;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Left joins another SQLite table on a matching column expression.
|
|
215
|
+
*/
|
|
216
|
+
leftJoin(table, firstCol, secondCol) {
|
|
217
|
+
this.joins.push({ type: 'LEFT', table, firstCol, secondCol });
|
|
218
|
+
return this;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Adds WHERE criteria to filter results.
|
|
222
|
+
* Supports:
|
|
223
|
+
* - where({ status: 'ACTIVE' })
|
|
224
|
+
* - where('status', 'ACTIVE')
|
|
225
|
+
* - where('end_date', '>=', today)
|
|
226
|
+
*/
|
|
227
|
+
where(columnOrFilter, operatorOrValue, value) {
|
|
228
|
+
if (typeof columnOrFilter === 'string') {
|
|
229
|
+
if (value !== undefined) {
|
|
230
|
+
const rawOp = String(operatorOrValue).toLowerCase().trim();
|
|
231
|
+
const opKey = rawOp === '>=' ? '$gte' :
|
|
232
|
+
rawOp === '<=' ? '$lte' :
|
|
233
|
+
rawOp === '>' ? '$gt' :
|
|
234
|
+
rawOp === '<' ? '$lt' :
|
|
235
|
+
rawOp === '!=' || rawOp === '<>' ? '$ne' :
|
|
236
|
+
rawOp === '=' || rawOp === '==' ? '$eq' :
|
|
237
|
+
rawOp === 'like' ? '$contains' : '$eq';
|
|
238
|
+
const existing = typeof this.whereConditions[columnOrFilter] === 'object' &&
|
|
239
|
+
this.whereConditions[columnOrFilter] !== null
|
|
240
|
+
? this.whereConditions[columnOrFilter]
|
|
241
|
+
: {};
|
|
242
|
+
this.whereConditions[columnOrFilter] = { ...existing, [opKey]: value };
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
this.whereConditions[columnOrFilter] = operatorOrValue;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
else if (columnOrFilter && typeof columnOrFilter === 'object') {
|
|
249
|
+
this.whereConditions = { ...this.whereConditions, ...columnOrFilter };
|
|
250
|
+
}
|
|
251
|
+
return this;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Orders query results by column and direction.
|
|
255
|
+
*/
|
|
256
|
+
orderBy(column, direction = 'ASC') {
|
|
257
|
+
this.orderColumn = column;
|
|
258
|
+
this.orderDirection = direction.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
|
|
259
|
+
return this;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Groups rows by one or more columns for aggregation.
|
|
263
|
+
*/
|
|
264
|
+
groupBy(...columns) {
|
|
265
|
+
this.groupByCols = columns.flat();
|
|
266
|
+
return this;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Limits the number of returned records.
|
|
270
|
+
*/
|
|
271
|
+
limit(count) {
|
|
272
|
+
this.limitCount = count;
|
|
273
|
+
return this;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Offsets the starting record position.
|
|
277
|
+
*/
|
|
278
|
+
offset(count) {
|
|
279
|
+
this.offsetCount = count;
|
|
280
|
+
return this;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Includes soft-deleted rows in results.
|
|
284
|
+
*/
|
|
285
|
+
withDeleted(include = true) {
|
|
286
|
+
this.includeDeleted = include;
|
|
287
|
+
return this;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Applies standard QueryOptions (select, where, orderBy, limit, offset, includeDeleted) to this query.
|
|
291
|
+
*/
|
|
292
|
+
applyOptions(options) {
|
|
293
|
+
if (!options)
|
|
294
|
+
return this;
|
|
295
|
+
if (options.select)
|
|
296
|
+
this.select(options.select);
|
|
297
|
+
if (options.where && typeof options.where === 'object')
|
|
298
|
+
this.where(options.where);
|
|
299
|
+
if (options.orderBy)
|
|
300
|
+
this.orderBy(options.orderBy, options.order || 'ASC');
|
|
301
|
+
if (typeof options.limit === 'number')
|
|
302
|
+
this.limit(options.limit);
|
|
303
|
+
if (typeof options.offset === 'number')
|
|
304
|
+
this.offset(options.offset);
|
|
305
|
+
if (options.includeDeleted)
|
|
306
|
+
this.withDeleted(true);
|
|
307
|
+
return this;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Compiles the current builder state into an executable SQL string and parameter list.
|
|
311
|
+
* Uses module-scoped compileComparison / compileWhereConditions helpers (defined once).
|
|
312
|
+
*/
|
|
313
|
+
toSQL() {
|
|
314
|
+
const params = [];
|
|
315
|
+
const projections = this.selectCols.map(formatColumnIdentifier).join(', ');
|
|
316
|
+
const fromTable = `"${this.tableName.replace(/"/g, '""')}"`;
|
|
317
|
+
let sql = `SELECT ${projections} FROM ${fromTable}`;
|
|
318
|
+
// Append JOINs
|
|
319
|
+
for (const j of this.joins) {
|
|
320
|
+
const joinTable = `"${j.table.replace(/"/g, '""')}"`;
|
|
321
|
+
const col1 = formatColumnIdentifier(j.firstCol);
|
|
322
|
+
const col2 = formatColumnIdentifier(j.secondCol);
|
|
323
|
+
sql += ` ${j.type} JOIN ${joinTable} ON ${col1} = ${col2}`;
|
|
324
|
+
}
|
|
325
|
+
// Build WHERE clauses (delegates to module-scoped helpers)
|
|
326
|
+
const whereClauses = [];
|
|
327
|
+
if (this.whereConditions && typeof this.whereConditions === 'object') {
|
|
328
|
+
whereClauses.push(...compileWhereConditions(this.whereConditions, params));
|
|
329
|
+
}
|
|
330
|
+
// Push down soft-deletion check on primary table if deleted column exists and not explicitly overridden
|
|
331
|
+
const schemaManager = 'schemaManager' in this.client
|
|
332
|
+
? this.client.schemaManager
|
|
333
|
+
: this.client.parent?.schemaManager;
|
|
334
|
+
if (!this.includeDeleted && schemaManager?.hasColumn(this.tableName, 'deleted')) {
|
|
335
|
+
const delCol = this.tableName.includes('.')
|
|
336
|
+
? `${formatColumnIdentifier(this.tableName)}."deleted"`
|
|
337
|
+
: `"${this.tableName.replace(/"/g, '""')}"."deleted"`;
|
|
338
|
+
whereClauses.push(`(${delCol} IS NULL OR (${delCol} != 1 AND ${delCol} != 'true'))`);
|
|
339
|
+
}
|
|
340
|
+
if (whereClauses.length > 0) {
|
|
341
|
+
sql += ` WHERE ${whereClauses.join(' AND ')}`;
|
|
342
|
+
}
|
|
343
|
+
// GROUP BY
|
|
344
|
+
if (this.groupByCols.length > 0) {
|
|
345
|
+
sql += ` GROUP BY ${this.groupByCols.map(formatColumnIdentifier).join(', ')}`;
|
|
346
|
+
}
|
|
347
|
+
// ORDER BY
|
|
348
|
+
if (this.orderColumn) {
|
|
349
|
+
sql += ` ORDER BY ${formatColumnIdentifier(this.orderColumn)} ${this.orderDirection}`;
|
|
350
|
+
}
|
|
351
|
+
// LIMIT & OFFSET
|
|
352
|
+
if (typeof this.limitCount === 'number' && this.limitCount > 0) {
|
|
353
|
+
sql += ` LIMIT ?`;
|
|
354
|
+
params.push(this.limitCount);
|
|
355
|
+
if (typeof this.offsetCount === 'number' && this.offsetCount > 0) {
|
|
356
|
+
sql += ` OFFSET ?`;
|
|
357
|
+
params.push(this.offsetCount);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return { sql, params };
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Executes the compiled SQL query against SQLite and returns rows.
|
|
364
|
+
*/
|
|
365
|
+
async execute() {
|
|
366
|
+
const schemaManager = 'schemaManager' in this.client
|
|
367
|
+
? this.client.schemaManager
|
|
368
|
+
: this.client.parent?.schemaManager;
|
|
369
|
+
const db = 'db' in this.client ? this.client.db : this.client.tx;
|
|
370
|
+
if (schemaManager && db) {
|
|
371
|
+
await schemaManager.ensureTableIntrospected(db, this.tableName);
|
|
372
|
+
for (const j of this.joins) {
|
|
373
|
+
await schemaManager.ensureTableIntrospected(db, j.table);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const { sql, params } = this.toSQL();
|
|
377
|
+
try {
|
|
378
|
+
const rawRows = await this.client.query(sql, params);
|
|
379
|
+
const hydrator = 'hydrateRow' in this.client
|
|
380
|
+
? this.client.hydrateRow.bind(this.client)
|
|
381
|
+
: this.client.parent?.hydrateRow?.bind(this.client.parent);
|
|
382
|
+
if (hydrator && rawRows && rawRows.length > 0) {
|
|
383
|
+
return rawRows.map((r) => hydrator(this.tableName, r));
|
|
384
|
+
}
|
|
385
|
+
return rawRows;
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
const msg = String(err?.message || err || '');
|
|
389
|
+
if (msg.includes('no such table') || msg.includes('no such column')) {
|
|
390
|
+
return [];
|
|
391
|
+
}
|
|
392
|
+
throw err;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Returns the first matching record or null if not found.
|
|
397
|
+
*/
|
|
398
|
+
async first() {
|
|
399
|
+
const originalLimit = this.limitCount;
|
|
400
|
+
this.limitCount = 1;
|
|
401
|
+
const rows = await this.execute();
|
|
402
|
+
this.limitCount = originalLimit;
|
|
403
|
+
return rows.length > 0 ? rows[0] : null;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Executes a COUNT(*) in SQLite and returns the numeric count directly.
|
|
407
|
+
*/
|
|
408
|
+
async count() {
|
|
409
|
+
const originalSelect = this.selectCols;
|
|
410
|
+
this.selectCols = ['COUNT(*) AS _count'];
|
|
411
|
+
const rows = await this.execute();
|
|
412
|
+
this.selectCols = originalSelect;
|
|
413
|
+
const countVal = rows[0]?._count;
|
|
414
|
+
return typeof countVal === 'number' ? countVal : Number(countVal) || 0;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Executes a SUM(column) in SQLite and returns the numeric total directly.
|
|
418
|
+
*/
|
|
419
|
+
async sum(column) {
|
|
420
|
+
const originalSelect = this.selectCols;
|
|
421
|
+
const safeCol = formatColumnIdentifier(column);
|
|
422
|
+
this.selectCols = [`COALESCE(SUM(${safeCol}), 0) AS _sum`];
|
|
423
|
+
const rows = await this.execute();
|
|
424
|
+
this.selectCols = originalSelect;
|
|
425
|
+
const sumVal = rows[0]?._sum;
|
|
426
|
+
return typeof sumVal === 'number' ? sumVal : Number(sumVal) || 0;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Executes an AVG(column) in SQLite and returns the average directly.
|
|
430
|
+
*/
|
|
431
|
+
async avg(column) {
|
|
432
|
+
const originalSelect = this.selectCols;
|
|
433
|
+
const safeCol = formatColumnIdentifier(column);
|
|
434
|
+
this.selectCols = [`AVG(${safeCol}) AS _avg`];
|
|
435
|
+
const rows = await this.execute();
|
|
436
|
+
this.selectCols = originalSelect;
|
|
437
|
+
const avgVal = rows[0]?._avg;
|
|
438
|
+
return avgVal === null || avgVal === undefined ? null : Number(avgVal) || 0;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
exports.QueryBuilder = QueryBuilder;
|
package/dist/core/query.js
CHANGED
|
@@ -48,26 +48,14 @@ function matchesField(fieldValue, filter) {
|
|
|
48
48
|
if (ops.$lte !== undefined && !(fieldValue <= ops.$lte)) {
|
|
49
49
|
return false;
|
|
50
50
|
}
|
|
51
|
-
if (ops.$contains !== undefined) {
|
|
52
|
-
const str = fieldValue
|
|
53
|
-
|
|
54
|
-
if (!str.includes(search)) {
|
|
51
|
+
if (ops.$contains !== undefined || ops.$startsWith !== undefined || ops.$endsWith !== undefined) {
|
|
52
|
+
const str = fieldValue == null ? '' : String(fieldValue).toLowerCase();
|
|
53
|
+
if (ops.$contains !== undefined && !str.includes(String(ops.$contains).toLowerCase()))
|
|
55
54
|
return false;
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
if (ops.$startsWith !== undefined) {
|
|
59
|
-
const str = fieldValue === null || fieldValue === undefined ? '' : String(fieldValue).toLowerCase();
|
|
60
|
-
const prefix = String(ops.$startsWith).toLowerCase();
|
|
61
|
-
if (!str.startsWith(prefix)) {
|
|
55
|
+
if (ops.$startsWith !== undefined && !str.startsWith(String(ops.$startsWith).toLowerCase()))
|
|
62
56
|
return false;
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
if (ops.$endsWith !== undefined) {
|
|
66
|
-
const str = fieldValue === null || fieldValue === undefined ? '' : String(fieldValue).toLowerCase();
|
|
67
|
-
const suffix = String(ops.$endsWith).toLowerCase();
|
|
68
|
-
if (!str.endsWith(suffix)) {
|
|
57
|
+
if (ops.$endsWith !== undefined && !str.endsWith(String(ops.$endsWith).toLowerCase()))
|
|
69
58
|
return false;
|
|
70
|
-
}
|
|
71
59
|
}
|
|
72
60
|
if (ops.$null !== undefined) {
|
|
73
61
|
const isNull = fieldValue === null || fieldValue === undefined;
|
|
@@ -131,18 +119,28 @@ function hasExplicitDeletedFilter(where) {
|
|
|
131
119
|
function applyQueryOptions(rows, options) {
|
|
132
120
|
if (!rows || rows.length === 0)
|
|
133
121
|
return [];
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
122
|
+
const needsDeleteFilter = !options?.includeDeleted && !hasExplicitDeletedFilter(options?.where);
|
|
123
|
+
const needsWhere = Boolean(options?.where);
|
|
124
|
+
const needsSort = Boolean(options?.orderBy);
|
|
125
|
+
const needsSlice = (options?.offset !== undefined && options.offset > 0) ||
|
|
126
|
+
(options?.limit !== undefined && options.limit > 0);
|
|
127
|
+
const needsSelect = Array.isArray(options?.select) && options.select.length > 0;
|
|
128
|
+
// Fast-path: nothing to do
|
|
129
|
+
if (!needsDeleteFilter && !needsWhere && !needsSort && !needsSlice && !needsSelect) {
|
|
130
|
+
return rows;
|
|
131
|
+
}
|
|
132
|
+
let result = rows;
|
|
133
|
+
// 0. Soft-delete filtering
|
|
134
|
+
if (needsDeleteFilter) {
|
|
138
135
|
result = result.filter((row) => !isDeletedRecord(row));
|
|
139
136
|
}
|
|
140
137
|
// 1. Where filtering
|
|
141
|
-
if (
|
|
138
|
+
if (needsWhere) {
|
|
142
139
|
result = result.filter((row) => matchesWhere(row, options.where));
|
|
143
140
|
}
|
|
144
141
|
// 2. Order by
|
|
145
|
-
if (
|
|
142
|
+
if (needsSort) {
|
|
143
|
+
result = [...result]; // copy only before sort mutation
|
|
146
144
|
const key = options.orderBy;
|
|
147
145
|
const isAsc = (options.order || 'asc').toLowerCase() === 'asc';
|
|
148
146
|
result.sort((a, b) => {
|
|
@@ -164,21 +162,19 @@ function applyQueryOptions(rows, options) {
|
|
|
164
162
|
});
|
|
165
163
|
}
|
|
166
164
|
// 3. Offset and Limit
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
result = result.slice(0, options.limit);
|
|
165
|
+
if (needsSlice) {
|
|
166
|
+
const offset = options?.offset && options.offset > 0 ? options.offset : 0;
|
|
167
|
+
const limit = options?.limit && options.limit > 0 ? options.limit : undefined;
|
|
168
|
+
result = limit !== undefined ? result.slice(offset, offset + limit) : result.slice(offset);
|
|
172
169
|
}
|
|
173
|
-
// 4. Column
|
|
174
|
-
if (
|
|
170
|
+
// 4. Column projection
|
|
171
|
+
if (needsSelect) {
|
|
175
172
|
const fields = options.select;
|
|
176
173
|
result = result.map((row) => {
|
|
177
174
|
const projected = {};
|
|
178
175
|
for (const field of fields) {
|
|
179
|
-
if (field in row)
|
|
176
|
+
if (field in row)
|
|
180
177
|
projected[field] = row[field];
|
|
181
|
-
}
|
|
182
178
|
}
|
|
183
179
|
return projected;
|
|
184
180
|
});
|