@inflector/optima 1.0.0 → 1.0.2
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/index.d.ts +323 -0
- package/dist/index.js +1155 -0
- package/package.json +11 -3
- package/src/Qfluent.ts +0 -179
- package/src/database.ts +0 -284
- package/src/fluent.ts +0 -55
- package/src/index.ts +0 -14
- package/src/schema.ts +0 -446
- package/src/table.ts +0 -926
- package/test/Schema.ts +0 -39
- package/test/index.ts +0 -16
- package/tsconfig.json +0 -15
package/src/table.ts
DELETED
|
@@ -1,926 +0,0 @@
|
|
|
1
|
-
import type { DuckDBConnection, DuckDBValue } from "@duckdb/node-api";
|
|
2
|
-
import {
|
|
3
|
-
SQLBuilder,
|
|
4
|
-
type ColumnBuilder,
|
|
5
|
-
type Infer,
|
|
6
|
-
type InferAdd,
|
|
7
|
-
type Prettify,
|
|
8
|
-
} from "./schema";
|
|
9
|
-
import { createFluentBuilder } from "./fluent";
|
|
10
|
-
import {
|
|
11
|
-
createProxyHandler,
|
|
12
|
-
createQueryBuilder,
|
|
13
|
-
createQueryBuilderOne,
|
|
14
|
-
type FluentQueryBuilder,
|
|
15
|
-
type FluentQueryBuilderOne,
|
|
16
|
-
type MapToFalse,
|
|
17
|
-
type QueryMethods,
|
|
18
|
-
type QueryOneMethods,
|
|
19
|
-
} from "./Qfluent";
|
|
20
|
-
|
|
21
|
-
// ---------------------------------------------------------
|
|
22
|
-
// 1. OPERATOR DEFINITIONS (Unchanged)
|
|
23
|
-
// ---------------------------------------------------------
|
|
24
|
-
|
|
25
|
-
export const OPS = [
|
|
26
|
-
"eq",
|
|
27
|
-
"ne",
|
|
28
|
-
"gt",
|
|
29
|
-
"gte",
|
|
30
|
-
"lt",
|
|
31
|
-
"lte",
|
|
32
|
-
"like",
|
|
33
|
-
"notLike",
|
|
34
|
-
"in",
|
|
35
|
-
"notIn",
|
|
36
|
-
"is",
|
|
37
|
-
"isNot",
|
|
38
|
-
"between",
|
|
39
|
-
"notBetween",
|
|
40
|
-
"startsWith",
|
|
41
|
-
"endsWith",
|
|
42
|
-
"contains",
|
|
43
|
-
"regexp",
|
|
44
|
-
"notRegexp",
|
|
45
|
-
] as const;
|
|
46
|
-
|
|
47
|
-
export type OpKey = (typeof OPS)[number];
|
|
48
|
-
|
|
49
|
-
// ---------------------------------------------------------
|
|
50
|
-
// 2. CONDITION BUILDER TYPES (Unchanged)
|
|
51
|
-
// ---------------------------------------------------------
|
|
52
|
-
|
|
53
|
-
type ConditionNode = {
|
|
54
|
-
type: "condition" | "and" | "or";
|
|
55
|
-
op?: OpKey;
|
|
56
|
-
value?: any;
|
|
57
|
-
left?: ConditionNode;
|
|
58
|
-
right?: ConditionNode;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
export interface ConditionBuilder<T> {
|
|
62
|
-
eq(value: T): ConditionBuilder<T>;
|
|
63
|
-
ne(value: T): ConditionBuilder<T>;
|
|
64
|
-
gt(value: T): ConditionBuilder<T>;
|
|
65
|
-
gte(value: T): ConditionBuilder<T>;
|
|
66
|
-
lt(value: T): ConditionBuilder<T>;
|
|
67
|
-
lte(value: T): ConditionBuilder<T>;
|
|
68
|
-
in(value: T[]): ConditionBuilder<T>;
|
|
69
|
-
notIn(value: T[]): ConditionBuilder<T>;
|
|
70
|
-
between(value: [T, T]): ConditionBuilder<T>;
|
|
71
|
-
notBetween(value: [T, T]): ConditionBuilder<T>;
|
|
72
|
-
is(value: T | null): ConditionBuilder<T>;
|
|
73
|
-
isNot(value: T | null): ConditionBuilder<T>;
|
|
74
|
-
like(value: string): ConditionBuilder<T>;
|
|
75
|
-
notLike(value: string): ConditionBuilder<T>;
|
|
76
|
-
startsWith(value: string): ConditionBuilder<T>;
|
|
77
|
-
endsWith(value: string): ConditionBuilder<T>;
|
|
78
|
-
contains(value: string): ConditionBuilder<T>;
|
|
79
|
-
regexp(value: string): ConditionBuilder<T>;
|
|
80
|
-
notRegexp(value: string): ConditionBuilder<T>;
|
|
81
|
-
and(other: ConditionBuilder<T>): ConditionBuilder<T>;
|
|
82
|
-
or(other: ConditionBuilder<T>): ConditionBuilder<T>;
|
|
83
|
-
__getNode(): ConditionNode;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
class ConditionBuilderImpl<T> implements ConditionBuilder<T> {
|
|
87
|
-
private node: ConditionNode;
|
|
88
|
-
|
|
89
|
-
constructor(node: ConditionNode) {
|
|
90
|
-
this.node = node;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
private createOp(op: OpKey, value: any): ConditionBuilder<T> {
|
|
94
|
-
return new ConditionBuilderImpl<T>({
|
|
95
|
-
type: "condition",
|
|
96
|
-
op,
|
|
97
|
-
value,
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
eq(value: T): ConditionBuilder<T> {
|
|
102
|
-
return this.createOp("eq", value);
|
|
103
|
-
}
|
|
104
|
-
ne(value: T): ConditionBuilder<T> {
|
|
105
|
-
return this.createOp("ne", value);
|
|
106
|
-
}
|
|
107
|
-
gt(value: T): ConditionBuilder<T> {
|
|
108
|
-
return this.createOp("gt", value);
|
|
109
|
-
}
|
|
110
|
-
gte(value: T): ConditionBuilder<T> {
|
|
111
|
-
return this.createOp("gte", value);
|
|
112
|
-
}
|
|
113
|
-
lt(value: T): ConditionBuilder<T> {
|
|
114
|
-
return this.createOp("lt", value);
|
|
115
|
-
}
|
|
116
|
-
lte(value: T): ConditionBuilder<T> {
|
|
117
|
-
return this.createOp("lte", value);
|
|
118
|
-
}
|
|
119
|
-
in(value: T[]): ConditionBuilder<T> {
|
|
120
|
-
return this.createOp("in", value);
|
|
121
|
-
}
|
|
122
|
-
notIn(value: T[]): ConditionBuilder<T> {
|
|
123
|
-
return this.createOp("notIn", value);
|
|
124
|
-
}
|
|
125
|
-
between(value: [T, T]): ConditionBuilder<T> {
|
|
126
|
-
return this.createOp("between", value);
|
|
127
|
-
}
|
|
128
|
-
notBetween(value: [T, T]): ConditionBuilder<T> {
|
|
129
|
-
return this.createOp("notBetween", value);
|
|
130
|
-
}
|
|
131
|
-
is(value: T | null): ConditionBuilder<T> {
|
|
132
|
-
return this.createOp("is", value);
|
|
133
|
-
}
|
|
134
|
-
isNot(value: T | null): ConditionBuilder<T> {
|
|
135
|
-
return this.createOp("isNot", value);
|
|
136
|
-
}
|
|
137
|
-
like(value: string): ConditionBuilder<T> {
|
|
138
|
-
return this.createOp("like", value);
|
|
139
|
-
}
|
|
140
|
-
notLike(value: string): ConditionBuilder<T> {
|
|
141
|
-
return this.createOp("notLike", value);
|
|
142
|
-
}
|
|
143
|
-
startsWith(value: string): ConditionBuilder<T> {
|
|
144
|
-
return this.createOp("startsWith", value);
|
|
145
|
-
}
|
|
146
|
-
endsWith(value: string): ConditionBuilder<T> {
|
|
147
|
-
return this.createOp("endsWith", value);
|
|
148
|
-
}
|
|
149
|
-
contains(value: string): ConditionBuilder<T> {
|
|
150
|
-
return this.createOp("contains", value);
|
|
151
|
-
}
|
|
152
|
-
regexp(value: string): ConditionBuilder<T> {
|
|
153
|
-
return this.createOp("regexp", value);
|
|
154
|
-
}
|
|
155
|
-
notRegexp(value: string): ConditionBuilder<T> {
|
|
156
|
-
return this.createOp("notRegexp", value);
|
|
157
|
-
}
|
|
158
|
-
and(other: ConditionBuilder<T>): ConditionBuilder<T> {
|
|
159
|
-
return new ConditionBuilderImpl<T>({
|
|
160
|
-
type: "and",
|
|
161
|
-
left: this.node,
|
|
162
|
-
right: other.__getNode(),
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
or(other: ConditionBuilder<T>): ConditionBuilder<T> {
|
|
167
|
-
return new ConditionBuilderImpl<T>({
|
|
168
|
-
type: "or",
|
|
169
|
-
left: this.node,
|
|
170
|
-
right: other.__getNode(),
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
__getNode(): ConditionNode {
|
|
175
|
-
return this.node;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export function cond<T>(): ConditionBuilder<T> {
|
|
180
|
-
return new ConditionBuilderImpl<T>({ type: "condition" }) as any;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Exports for standalone operators
|
|
184
|
-
export const eq = <T>(value: T) => cond<T>().eq(value);
|
|
185
|
-
export const ne = <T>(value: T) => cond<T>().ne(value);
|
|
186
|
-
export const gt = <T>(value: T) => cond<T>().gt(value);
|
|
187
|
-
export const gte = <T>(value: T) => cond<T>().gte(value);
|
|
188
|
-
export const lt = <T>(value: T) => cond<T>().lt(value);
|
|
189
|
-
export const lte = <T>(value: T) => cond<T>().lte(value);
|
|
190
|
-
export const inOp = <T>(value: T[]) => cond<T>().in(value);
|
|
191
|
-
export const notIn = <T>(value: T[]) => cond<T>().notIn(value);
|
|
192
|
-
export const between = <T>(value: [T, T]) => cond<T>().between(value);
|
|
193
|
-
export const notBetween = <T>(value: [T, T]) => cond<T>().notBetween(value);
|
|
194
|
-
export const is = <T>(value: T | null) => cond<T>().is(value);
|
|
195
|
-
export const isNot = <T>(value: T | null) => cond<T>().isNot(value);
|
|
196
|
-
export const like = <T>(value: string) => cond<T>().like(value);
|
|
197
|
-
export const notLike = <T>(value: string) => cond<T>().notLike(value);
|
|
198
|
-
export const startsWith = <T>(value: string) => cond<T>().startsWith(value);
|
|
199
|
-
export const endsWith = <T>(value: string) => cond<T>().endsWith(value);
|
|
200
|
-
export const contains = <T>(value: string) => cond<T>().contains(value);
|
|
201
|
-
export const regexp = <T>(value: string) => cond<T>().regexp(value);
|
|
202
|
-
export const notRegexp = <T>(value: string) => cond<T>().notRegexp(value);
|
|
203
|
-
|
|
204
|
-
// ---------------------------------------------------------
|
|
205
|
-
// 3. WHERE TYPE DEFINITIONS (Unchanged)
|
|
206
|
-
// ---------------------------------------------------------
|
|
207
|
-
|
|
208
|
-
export type FieldQuery<T> =
|
|
209
|
-
// If it's ConditionBuilder already, allow it directly
|
|
210
|
-
T extends ConditionBuilder<infer U>
|
|
211
|
-
? T
|
|
212
|
-
: // Arrays should be passed as is (for IN, etc), not recursed
|
|
213
|
-
T extends Array<any>
|
|
214
|
-
? T | ConditionBuilder<T>
|
|
215
|
-
: // Date is an object in JS, but we treat it as a primitive for SQL
|
|
216
|
-
T extends Date
|
|
217
|
-
? T | ConditionBuilder<T>
|
|
218
|
-
: // For objects (Structs), allow recursion OR a condition on the whole struct
|
|
219
|
-
T extends object
|
|
220
|
-
? { [K in keyof T]?: FieldQuery<T[K]> } | ConditionBuilder<T>
|
|
221
|
-
: // Primitives
|
|
222
|
-
T | ConditionBuilder<T>;
|
|
223
|
-
|
|
224
|
-
export type Where<Schema> = {
|
|
225
|
-
[K in keyof Schema]?: FieldQuery<Schema[K]>;
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
// ---------------------------------------------------------
|
|
229
|
-
// 4. SQL GENERATION UTILITIES (Unchanged)
|
|
230
|
-
// ---------------------------------------------------------
|
|
231
|
-
|
|
232
|
-
const escape = (val: any): string => {
|
|
233
|
-
if (val === null || val === undefined) return "NULL";
|
|
234
|
-
if (typeof val === "string") return `'${val.replace(/'/g, "''")}'`;
|
|
235
|
-
if (val instanceof Date) return `'${val.toISOString()}'`;
|
|
236
|
-
if (typeof val === "boolean") return val ? "TRUE" : "FALSE";
|
|
237
|
-
if (typeof val === "object")
|
|
238
|
-
return `'${JSON.stringify(val).replace(/'/g, "''")}'`;
|
|
239
|
-
return String(val);
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
const SQL_GENERATORS: Record<OpKey, (col: string, val: any) => string> = {
|
|
243
|
-
eq: (c, v) => `${c} = ${escape(v)}`,
|
|
244
|
-
ne: (c, v) => `${c} != ${escape(v)}`,
|
|
245
|
-
gt: (c, v) => `${c} > ${escape(v)}`,
|
|
246
|
-
gte: (c, v) => `${c} >= ${escape(v)}`,
|
|
247
|
-
lt: (c, v) => `${c} < ${escape(v)}`,
|
|
248
|
-
lte: (c, v) => `${c} <= ${escape(v)}`,
|
|
249
|
-
in: (c, v) => `${c} IN (${(v as any[]).map(escape).join(", ")})`,
|
|
250
|
-
notIn: (c, v) => `${c} NOT IN (${(v as any[]).map(escape).join(", ")})`,
|
|
251
|
-
between: (c, v) => `${c} BETWEEN ${escape(v[0])} AND ${escape(v[1])}`,
|
|
252
|
-
notBetween: (c, v) => `${c} NOT BETWEEN ${escape(v[0])} AND ${escape(v[1])}`,
|
|
253
|
-
is: (c, v) => (v === null ? `${c} IS NULL` : `${c} IS ${escape(v)}`),
|
|
254
|
-
isNot: (c, v) =>
|
|
255
|
-
v === null ? `${c} IS NOT NULL` : `${c} IS NOT ${escape(v)}`,
|
|
256
|
-
like: (c, v) => `${c} LIKE ${escape(v)}`,
|
|
257
|
-
notLike: (c, v) => `${c} NOT LIKE ${escape(v)}`,
|
|
258
|
-
startsWith: (c, v) => `${c} LIKE ${escape(v + "%")}`,
|
|
259
|
-
endsWith: (c, v) => `${c} LIKE ${escape("%" + v)}`,
|
|
260
|
-
contains: (c, v) => `${c} LIKE ${escape("%" + v + "%")}`,
|
|
261
|
-
regexp: (c, v) => `regexp_matches(${c}, ${escape(v)})`,
|
|
262
|
-
notRegexp: (c, v) => `NOT regexp_matches(${c}, ${escape(v)})`,
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
export type TableSchema<T> = T extends OptimaTable<infer Schema>
|
|
266
|
-
? Prettify<Infer<Schema>>
|
|
267
|
-
: never;
|
|
268
|
-
|
|
269
|
-
// --- Extension Type Helpers ---
|
|
270
|
-
|
|
271
|
-
type GetRefSchema<Col> = Col extends ColumnBuilder<any, any, infer Ref>
|
|
272
|
-
? Ref
|
|
273
|
-
: Col extends { config: { reference?: { ref: string; isMany: boolean } } }
|
|
274
|
-
? never
|
|
275
|
-
: never;
|
|
276
|
-
|
|
277
|
-
type IsManyRef<RS> = RS extends { readonly __refKind: "many" } ? true : false;
|
|
278
|
-
|
|
279
|
-
type HasAnyManyRef<TDef> = true extends {
|
|
280
|
-
[K in keyof TDef]: GetRefSchema<TDef[K]> extends never
|
|
281
|
-
? never
|
|
282
|
-
: IsManyRef<GetRefSchema<TDef[K]>>;
|
|
283
|
-
}[keyof TDef]
|
|
284
|
-
? true
|
|
285
|
-
: false;
|
|
286
|
-
|
|
287
|
-
export type Extension<SourceDef, TargetTable> = TargetTable extends OptimaTable<
|
|
288
|
-
infer TargetDef
|
|
289
|
-
>
|
|
290
|
-
? TargetDef extends { __tableName: infer Name }
|
|
291
|
-
? Name extends string
|
|
292
|
-
? HasAnyManyRef<SourceDef> extends true
|
|
293
|
-
? { [K in `$${Name}`]: Infer<TargetDef>[] }
|
|
294
|
-
: { [K in `$${Name}`]: Infer<TargetDef> }
|
|
295
|
-
: {}
|
|
296
|
-
: {}
|
|
297
|
-
: {};
|
|
298
|
-
|
|
299
|
-
type Unsubscribe = () => void;
|
|
300
|
-
export class OptimaTable<TDef extends Record<string, any> = any> {
|
|
301
|
-
private Name: string;
|
|
302
|
-
private Connection: DuckDBConnection;
|
|
303
|
-
private Columns: TDef;
|
|
304
|
-
private listeners = new Set<
|
|
305
|
-
(change: {
|
|
306
|
-
event: "Add" | "AddMany" | "Delete" | "Update";
|
|
307
|
-
data: Partial<Infer<TDef>>;
|
|
308
|
-
time: Date;
|
|
309
|
-
}) => void
|
|
310
|
-
>();
|
|
311
|
-
private constructor(
|
|
312
|
-
name: string,
|
|
313
|
-
Columns: TDef,
|
|
314
|
-
Connection: DuckDBConnection
|
|
315
|
-
) {
|
|
316
|
-
this.Name = name;
|
|
317
|
-
const filteredCols: any = { ...Columns };
|
|
318
|
-
if ("__tableName" in filteredCols) {
|
|
319
|
-
delete filteredCols["__tableName"];
|
|
320
|
-
}
|
|
321
|
-
this.Columns = filteredCols;
|
|
322
|
-
this.Connection = Connection;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
protected notifyChange(change: any) {
|
|
326
|
-
this.listeners.forEach((listener) => listener(change));
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Subscribe to changes to the table.
|
|
331
|
-
* The callback receives the latest change as its argument.
|
|
332
|
-
* Returns an unsubscribe function.
|
|
333
|
-
*/
|
|
334
|
-
Subscribe(
|
|
335
|
-
fn: (change: {
|
|
336
|
-
event: "Add" | "AddMany" | "Delete" | "Update";
|
|
337
|
-
data: Partial<Infer<TDef>>;
|
|
338
|
-
time: Date;
|
|
339
|
-
}) => void
|
|
340
|
-
): Unsubscribe {
|
|
341
|
-
this.listeners.add(fn);
|
|
342
|
-
return () => {
|
|
343
|
-
this.listeners.delete(fn);
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
private async InitTable() {
|
|
348
|
-
const SQL = SQLBuilder.BuildTable(this.Name, this.Columns);
|
|
349
|
-
await this.Connection.run(SQL);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
static async create<T extends Record<string, ColumnBuilder<any, any>>>(
|
|
353
|
-
name: string,
|
|
354
|
-
Columns: T,
|
|
355
|
-
Connection: DuckDBConnection
|
|
356
|
-
): Promise<OptimaTable<T>> {
|
|
357
|
-
const table = new OptimaTable(name, Columns, Connection);
|
|
358
|
-
await table.InitTable();
|
|
359
|
-
return table;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// ---------------------------------------------------------
|
|
363
|
-
// UPDATED: Get() now uses the Fluent Factory
|
|
364
|
-
// ---------------------------------------------------------
|
|
365
|
-
Get(): FluentQueryBuilder<
|
|
366
|
-
TDef,
|
|
367
|
-
Infer<TDef>,
|
|
368
|
-
MapToFalse<QueryMethods<TDef>> & { extended: false }
|
|
369
|
-
> {
|
|
370
|
-
return createQueryBuilder(this);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// ---------------------------------------------------------
|
|
374
|
-
// UPDATED: GetOne() now uses the Fluent Factory
|
|
375
|
-
// ---------------------------------------------------------
|
|
376
|
-
GetOne(): FluentQueryBuilderOne<
|
|
377
|
-
TDef,
|
|
378
|
-
Infer<TDef>,
|
|
379
|
-
MapToFalse<QueryOneMethods<TDef>> & { extended: false }
|
|
380
|
-
> {
|
|
381
|
-
return createQueryBuilderOne(this);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
Exist() {
|
|
385
|
-
type TSchema = Infer<TDef>;
|
|
386
|
-
return createFluentBuilder<
|
|
387
|
-
{
|
|
388
|
-
where: Where<TSchema>;
|
|
389
|
-
},
|
|
390
|
-
boolean
|
|
391
|
-
>(async (data) => {
|
|
392
|
-
const whereClause = this.BuildWhereClause(data.where);
|
|
393
|
-
const sql = `SELECT 1 FROM ${this.Name}${whereClause} LIMIT 1`;
|
|
394
|
-
|
|
395
|
-
const Result: Record<string, DuckDBValue>[] = await (
|
|
396
|
-
await this.Connection.run(sql)
|
|
397
|
-
).getRowObjects();
|
|
398
|
-
|
|
399
|
-
return Result.length > 0;
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* Add a single record.
|
|
405
|
-
* Returns: TSchema[] (containing the inserted record if returning is true)
|
|
406
|
-
*/
|
|
407
|
-
Add(record: InferAdd<TDef>) {
|
|
408
|
-
type TSchema = Infer<TDef>;
|
|
409
|
-
return createFluentBuilder<
|
|
410
|
-
{
|
|
411
|
-
returning: boolean;
|
|
412
|
-
},
|
|
413
|
-
TSchema[]
|
|
414
|
-
>(async (data) => {
|
|
415
|
-
this.Validate(record);
|
|
416
|
-
record = this.Transform(record);
|
|
417
|
-
const { sql } = this.BuildInsert(this.Name, [record], data.returning);
|
|
418
|
-
const Result: Record<string, DuckDBValue>[] = await (
|
|
419
|
-
await this.Connection.run(sql)
|
|
420
|
-
).getRowObjects();
|
|
421
|
-
const Res = this.FormatOut(Result) as TSchema[];
|
|
422
|
-
if (Res.length != 0) {
|
|
423
|
-
if (data.returning) {
|
|
424
|
-
this.notifyChange({ event: "Add", data: Res, time: new Date() });
|
|
425
|
-
} else {
|
|
426
|
-
this.notifyChange({
|
|
427
|
-
event: "Add",
|
|
428
|
-
data: await this.GetOne().where(record as any),
|
|
429
|
-
time: new Date(),
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
return Res;
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* Add multiple records.
|
|
439
|
-
* Returns: TSchema[]
|
|
440
|
-
*/
|
|
441
|
-
AddMany(records: InferAdd<TDef>[]) {
|
|
442
|
-
type TSchema = Infer<TDef>;
|
|
443
|
-
return createFluentBuilder<
|
|
444
|
-
{
|
|
445
|
-
returning: boolean;
|
|
446
|
-
},
|
|
447
|
-
TSchema[]
|
|
448
|
-
>(async (data) => {
|
|
449
|
-
records.forEach((v) => this.Validate(v));
|
|
450
|
-
const transformedRecords = records.map((v) => this.Transform(v));
|
|
451
|
-
const { sql } = this.BuildInsert(
|
|
452
|
-
this.Name,
|
|
453
|
-
transformedRecords,
|
|
454
|
-
data.returning
|
|
455
|
-
);
|
|
456
|
-
const Result: Record<string, DuckDBValue>[] = await (
|
|
457
|
-
await this.Connection.run(sql)
|
|
458
|
-
).getRowObjects();
|
|
459
|
-
const Res = this.FormatOut(Result) as TSchema[];
|
|
460
|
-
if (Res.length != 0) {
|
|
461
|
-
if (data.returning) {
|
|
462
|
-
this.notifyChange({ event: "AddMany", data: Res, time: new Date() });
|
|
463
|
-
} else {
|
|
464
|
-
const Added = transformedRecords;
|
|
465
|
-
// Use Promise.all to retrieve the added records
|
|
466
|
-
const addedPromises = Added.map(
|
|
467
|
-
async (record) => await this.GetOne().where(record as any)
|
|
468
|
-
);
|
|
469
|
-
const addedRecords = await Promise.all(addedPromises);
|
|
470
|
-
this.notifyChange({
|
|
471
|
-
event: "Add",
|
|
472
|
-
data: addedRecords,
|
|
473
|
-
time: new Date(),
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
return Res;
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* Update records.
|
|
483
|
-
* Returns: TSchema[]
|
|
484
|
-
*/
|
|
485
|
-
Update(record: Partial<Infer<TDef>>) {
|
|
486
|
-
type TSchema = Infer<TDef>;
|
|
487
|
-
return createFluentBuilder<
|
|
488
|
-
{
|
|
489
|
-
where: Where<TSchema>;
|
|
490
|
-
returning: boolean;
|
|
491
|
-
},
|
|
492
|
-
TSchema[]
|
|
493
|
-
>(async (data) => {
|
|
494
|
-
this.Validate(record);
|
|
495
|
-
record = this.Transform(record);
|
|
496
|
-
const PotentialyUpdated = await this.Get().where(record as any);
|
|
497
|
-
if (PotentialyUpdated.length == 0) return [];
|
|
498
|
-
else {
|
|
499
|
-
const { sql } = this.BuildUpdate(this.Name, record, data);
|
|
500
|
-
const Result: Record<string, DuckDBValue>[] = await (
|
|
501
|
-
await this.Connection.run(sql)
|
|
502
|
-
).getRowObjects();
|
|
503
|
-
if (Result.length != 0) {
|
|
504
|
-
if (data.returning) {
|
|
505
|
-
this.notifyChange({ event: "AddMany", data: Result, time: new Date() });
|
|
506
|
-
} else {
|
|
507
|
-
const Updated = PotentialyUpdated as any[];
|
|
508
|
-
const UpdatedPromises = Updated.map(
|
|
509
|
-
async (record: any) => await this.GetOne().where(record as any)
|
|
510
|
-
);
|
|
511
|
-
const UpdatedRecords = await Promise.all(UpdatedPromises);
|
|
512
|
-
this.notifyChange({
|
|
513
|
-
event: "Add",
|
|
514
|
-
data: UpdatedRecords,
|
|
515
|
-
time: new Date(),
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
return (data.returning ? this.FormatOut(Result) : Result) as TSchema[];
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
/**
|
|
526
|
-
* Delete records.
|
|
527
|
-
* Returns: TSchema | undefined (The code logic returns index [0], implies single delete or returning first)
|
|
528
|
-
*/
|
|
529
|
-
Delete() {
|
|
530
|
-
type TSchema = Infer<TDef>;
|
|
531
|
-
return createFluentBuilder<
|
|
532
|
-
{
|
|
533
|
-
where: Where<TSchema>;
|
|
534
|
-
returning: boolean;
|
|
535
|
-
},
|
|
536
|
-
TSchema | undefined
|
|
537
|
-
>(async (data) => {
|
|
538
|
-
// Find what would be potentially deleted using the where clause
|
|
539
|
-
const PotentiallyDeleted = await this.Get().where(data.where as any);
|
|
540
|
-
if (PotentiallyDeleted.length == 0) return undefined;
|
|
541
|
-
const { sql } = this.BuildDelete(this.Name, { ...data });
|
|
542
|
-
const Result: Record<string, DuckDBValue>[] = await (
|
|
543
|
-
await this.Connection.run(sql)
|
|
544
|
-
).getRowObjects();
|
|
545
|
-
|
|
546
|
-
if (Result.length != 0) {
|
|
547
|
-
if (data.returning) {
|
|
548
|
-
this.notifyChange({ event: "DeleteMany", data: Result, time: new Date() });
|
|
549
|
-
} else {
|
|
550
|
-
const DeletedRecords = PotentiallyDeleted as any[];
|
|
551
|
-
this.notifyChange({
|
|
552
|
-
event: "Delete",
|
|
553
|
-
data: DeletedRecords,
|
|
554
|
-
time: new Date(),
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
const formatted = this.FormatOut(Result) as TSchema[];
|
|
559
|
-
return formatted[0];
|
|
560
|
-
});
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
Count() {
|
|
564
|
-
type TSchema = Infer<TDef>;
|
|
565
|
-
return createFluentBuilder<{ where: Where<TSchema> }, number>(
|
|
566
|
-
async (data) => {
|
|
567
|
-
const whereClause = this.BuildWhereClause(data.where);
|
|
568
|
-
const sql = `SELECT COUNT(*) as count FROM ${this.Name}${whereClause}`;
|
|
569
|
-
const result = await (await this.Connection.run(sql)).getRowObjects();
|
|
570
|
-
// @ts-ignore
|
|
571
|
-
return Number(result[0].count);
|
|
572
|
-
}
|
|
573
|
-
);
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
/**
|
|
577
|
-
* Auto-resolves relationships based on schema 'reference'
|
|
578
|
-
*/
|
|
579
|
-
private ResolveJoin(
|
|
580
|
-
targetTable: OptimaTable<any>
|
|
581
|
-
): { sql: string; isMany: boolean } | null {
|
|
582
|
-
const myName = this.Name;
|
|
583
|
-
const targetName = targetTable.Name;
|
|
584
|
-
|
|
585
|
-
const getRef = (builder: any) => {
|
|
586
|
-
// @ts-ignore
|
|
587
|
-
const refConf = builder.config.reference;
|
|
588
|
-
if (typeof refConf === "string") return { ref: refConf, isMany: false };
|
|
589
|
-
if (refConf && typeof refConf === "object") return refConf;
|
|
590
|
-
return null;
|
|
591
|
-
};
|
|
592
|
-
|
|
593
|
-
// // 1. They reference Me
|
|
594
|
-
// for (const [colName, builder] of Object.entries(targetTable.Columns)) {
|
|
595
|
-
// const config = getRef(builder);
|
|
596
|
-
// if (config && config.ref) {
|
|
597
|
-
// const [refTable, refCol] = config.ref.split(".");
|
|
598
|
-
// if (refTable === myName) {
|
|
599
|
-
// // if (config.isMany) {
|
|
600
|
-
// // return `list_contains(${targetName}.${colName}, ${myName}.${refCol})`;
|
|
601
|
-
// // }
|
|
602
|
-
// return `${targetName}.${colName} = ${myName}.${refCol}`;
|
|
603
|
-
// }
|
|
604
|
-
// }
|
|
605
|
-
// }
|
|
606
|
-
|
|
607
|
-
// 2. I reference Them
|
|
608
|
-
for (const [colName, builder] of Object.entries(this.Columns)) {
|
|
609
|
-
const config = getRef(builder);
|
|
610
|
-
if (config && config.ref) {
|
|
611
|
-
const [refTable, refCol] = config.ref.split(".");
|
|
612
|
-
if (refTable === targetName) {
|
|
613
|
-
// if (config.isMany) {
|
|
614
|
-
// return `list_contains(${myName}.${colName}, ${targetName}.${refCol})`;
|
|
615
|
-
// }
|
|
616
|
-
return {
|
|
617
|
-
sql: `${myName}.${colName} = ${targetName}.${refCol}`,
|
|
618
|
-
isMany: config.isMany,
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
console.warn(
|
|
625
|
-
`Could not resolve relationship between ${myName} and ${targetName}`
|
|
626
|
-
);
|
|
627
|
-
return null;
|
|
628
|
-
}
|
|
629
|
-
// -- Validator --
|
|
630
|
-
private Validate(data: any) {
|
|
631
|
-
const Cols = Object.fromEntries(
|
|
632
|
-
//@ts-ignore
|
|
633
|
-
Object.entries(this.Columns).map(([col, val]) => [col, val.config])
|
|
634
|
-
);
|
|
635
|
-
for (const [field, config] of Object.entries(Cols)) {
|
|
636
|
-
// For partial updates, skip validation if key is missing
|
|
637
|
-
if (data[field] === undefined) continue;
|
|
638
|
-
|
|
639
|
-
if ("validate" in config && typeof config.validate === "function") {
|
|
640
|
-
const fn = config.validate;
|
|
641
|
-
if (!fn(data[field])) {
|
|
642
|
-
throw new Error(
|
|
643
|
-
`Validation failed for field '${field}' : The value '${JSON.stringify(
|
|
644
|
-
data[field]
|
|
645
|
-
)}' doesn't pass the validate function`
|
|
646
|
-
);
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
// -- Transformer --
|
|
652
|
-
private Transform(data: any) {
|
|
653
|
-
const Cols = Object.fromEntries(
|
|
654
|
-
//@ts-ignore
|
|
655
|
-
Object.entries(this.Columns).map(([col, val]) => [col, val.config])
|
|
656
|
-
);
|
|
657
|
-
const transformed: any = { ...data };
|
|
658
|
-
for (const [field, config] of Object.entries(Cols)) {
|
|
659
|
-
// For partial updates, skip transform if key is missing
|
|
660
|
-
if (transformed[field] === undefined) continue;
|
|
661
|
-
|
|
662
|
-
if ("transform" in config && typeof config.transform === "function") {
|
|
663
|
-
const fn = config.transform;
|
|
664
|
-
transformed[field] = fn(transformed[field]);
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
return transformed;
|
|
668
|
-
}
|
|
669
|
-
// --- SHARED: Where Clause Builder ---
|
|
670
|
-
private BuildWhereClause = (
|
|
671
|
-
where: Where<Infer<TDef>> | undefined
|
|
672
|
-
): string => {
|
|
673
|
-
if (!where) return "";
|
|
674
|
-
|
|
675
|
-
const conditions: string[] = [];
|
|
676
|
-
|
|
677
|
-
// Helper to generate SQL for specific operators
|
|
678
|
-
const nodeToSQL = (node: ConditionNode, colName: string): string => {
|
|
679
|
-
if (node.type === "condition" && node.op) {
|
|
680
|
-
return SQL_GENERATORS[node.op](colName, node.value);
|
|
681
|
-
} else if (node.type === "and" && node.left && node.right) {
|
|
682
|
-
return `(${nodeToSQL(node.left, colName)} AND ${nodeToSQL(
|
|
683
|
-
node.right,
|
|
684
|
-
colName
|
|
685
|
-
)})`;
|
|
686
|
-
} else if (node.type === "or" && node.left && node.right) {
|
|
687
|
-
return `(${nodeToSQL(node.left, colName)} OR ${nodeToSQL(
|
|
688
|
-
node.right,
|
|
689
|
-
colName
|
|
690
|
-
)})`;
|
|
691
|
-
}
|
|
692
|
-
return "";
|
|
693
|
-
};
|
|
694
|
-
|
|
695
|
-
// Recursive function to handle nested objects (Structs)
|
|
696
|
-
const processLevel = (currentWhere: any, pathPrefix: string) => {
|
|
697
|
-
for (const key of Object.keys(currentWhere)) {
|
|
698
|
-
const val = currentWhere[key];
|
|
699
|
-
|
|
700
|
-
// Build the dot-notation column name (e.g. "address.city")
|
|
701
|
-
const colName = pathPrefix ? `${pathPrefix}.${key}` : key;
|
|
702
|
-
|
|
703
|
-
// 1. Skip undefined values
|
|
704
|
-
if (val === undefined) continue;
|
|
705
|
-
|
|
706
|
-
// 2. Check for ConditionBuilder (e.g., eq(5), is(null))
|
|
707
|
-
if (
|
|
708
|
-
val &&
|
|
709
|
-
typeof val === "object" &&
|
|
710
|
-
"__getNode" in val &&
|
|
711
|
-
typeof val.__getNode === "function"
|
|
712
|
-
) {
|
|
713
|
-
const node = (val as ConditionBuilder<any>).__getNode();
|
|
714
|
-
const sql = nodeToSQL(node, colName);
|
|
715
|
-
if (sql) conditions.push(sql);
|
|
716
|
-
}
|
|
717
|
-
// 3. Check for Nested Object (Struct recursion)
|
|
718
|
-
// Must exclude Arrays and Dates which are treated as values
|
|
719
|
-
else if (
|
|
720
|
-
val &&
|
|
721
|
-
typeof val === "object" &&
|
|
722
|
-
!Array.isArray(val) &&
|
|
723
|
-
!(val instanceof Date)
|
|
724
|
-
) {
|
|
725
|
-
processLevel(val, colName);
|
|
726
|
-
}
|
|
727
|
-
// 4. Direct Value (Implicit Equality)
|
|
728
|
-
else {
|
|
729
|
-
conditions.push(SQL_GENERATORS.eq(colName, val));
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
};
|
|
733
|
-
|
|
734
|
-
processLevel(where, "");
|
|
735
|
-
|
|
736
|
-
if (conditions.length > 0) {
|
|
737
|
-
return ` WHERE ${conditions.join(" AND ")}`;
|
|
738
|
-
}
|
|
739
|
-
return "";
|
|
740
|
-
};
|
|
741
|
-
|
|
742
|
-
private BuildSelect = (
|
|
743
|
-
TableName: string,
|
|
744
|
-
options: {
|
|
745
|
-
limit?: number;
|
|
746
|
-
orderBy?: [keyof TDef | Array<keyof TDef>, "ASC" | "DESC"];
|
|
747
|
-
offset?: number;
|
|
748
|
-
groupBy?: Array<keyof TDef>;
|
|
749
|
-
where: Where<Infer<TDef>>;
|
|
750
|
-
extend?: OptimaTable<any> | OptimaTable<any>[];
|
|
751
|
-
}
|
|
752
|
-
) => {
|
|
753
|
-
const { limit, orderBy, offset, groupBy, where, extend } = options;
|
|
754
|
-
|
|
755
|
-
// 1. Base Columns
|
|
756
|
-
let selectColumns = `${TableName}.*`;
|
|
757
|
-
|
|
758
|
-
// 2. Extended Columns (DuckDB Correlated Subquery)
|
|
759
|
-
if (extend) {
|
|
760
|
-
const extensions = Array.isArray(extend) ? extend : [extend];
|
|
761
|
-
|
|
762
|
-
for (const extTable of extensions) {
|
|
763
|
-
const joinLogic = this.ResolveJoin(extTable);
|
|
764
|
-
if (joinLogic) {
|
|
765
|
-
selectColumns += `, (SELECT ${joinLogic.isMany ? "list(" : ""}${extTable.Name
|
|
766
|
-
}${joinLogic.isMany ? ")" : ""} FROM ${extTable.Name} WHERE ${joinLogic.sql
|
|
767
|
-
}) AS "$${extTable.Name}"`;
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
let query = `SELECT ${selectColumns} FROM ${TableName}`;
|
|
773
|
-
|
|
774
|
-
query += this.BuildWhereClause(where);
|
|
775
|
-
|
|
776
|
-
if (groupBy && groupBy.length > 0)
|
|
777
|
-
query += ` GROUP BY ${groupBy.join(", ")}`;
|
|
778
|
-
|
|
779
|
-
if (orderBy) {
|
|
780
|
-
const [columns, direction] = orderBy;
|
|
781
|
-
const columnStr = Array.isArray(columns) ? columns.join(", ") : columns;
|
|
782
|
-
query += ` ORDER BY ${String(columnStr)} ${direction}`;
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
if (limit !== undefined && limit !== null) query += ` LIMIT ${limit}`;
|
|
786
|
-
if (offset !== undefined && offset !== null) query += ` OFFSET ${offset}`;
|
|
787
|
-
|
|
788
|
-
return { sql: query, args: [] };
|
|
789
|
-
};
|
|
790
|
-
|
|
791
|
-
private BuildInsert = (
|
|
792
|
-
TableName: string,
|
|
793
|
-
Records: InferAdd<TDef>[],
|
|
794
|
-
isReturning: boolean
|
|
795
|
-
) => {
|
|
796
|
-
const columnKeys = Object.keys(this.Columns);
|
|
797
|
-
const columnsHeader = `(${columnKeys.join(", ")})`;
|
|
798
|
-
const valuesBlock = Records.map((r) => {
|
|
799
|
-
const orderedValues = columnKeys.map((key) => {
|
|
800
|
-
let v = (r as any)[key];
|
|
801
|
-
if (v == null && this.Columns[key].config.default) {
|
|
802
|
-
v = this.Columns[key].config.default();
|
|
803
|
-
}
|
|
804
|
-
return escape(v);
|
|
805
|
-
});
|
|
806
|
-
return `(${orderedValues.join(",")})`;
|
|
807
|
-
}).join(",\n");
|
|
808
|
-
const sql = `INSERT INTO ${TableName} ${columnsHeader} VALUES \n${valuesBlock}${isReturning ? "\nRETURNING *" : ""
|
|
809
|
-
};`;
|
|
810
|
-
|
|
811
|
-
return { sql: sql };
|
|
812
|
-
};
|
|
813
|
-
|
|
814
|
-
private BuildUpdate = (
|
|
815
|
-
TableName: string,
|
|
816
|
-
changes: Partial<Infer<TDef>>,
|
|
817
|
-
options: {
|
|
818
|
-
where: Where<Infer<TDef>>;
|
|
819
|
-
returning: boolean;
|
|
820
|
-
}
|
|
821
|
-
) => {
|
|
822
|
-
const { where, returning } = options;
|
|
823
|
-
|
|
824
|
-
const setClauses: string[] = [];
|
|
825
|
-
for (const [key, value] of Object.entries(changes)) {
|
|
826
|
-
if (value !== undefined) {
|
|
827
|
-
setClauses.push(`${key} = ${escape(value)}`);
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
if (setClauses.length === 0) {
|
|
832
|
-
throw new Error("No fields provided to update.");
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
let query = `UPDATE ${TableName} SET ${setClauses.join(", ")}`;
|
|
836
|
-
|
|
837
|
-
query += this.BuildWhereClause(where);
|
|
838
|
-
|
|
839
|
-
if (returning) {
|
|
840
|
-
query += " RETURNING *";
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
return { sql: query, args: [] };
|
|
844
|
-
};
|
|
845
|
-
|
|
846
|
-
private BuildDelete = (
|
|
847
|
-
TableName: string,
|
|
848
|
-
options: {
|
|
849
|
-
where: Where<Infer<TDef>>;
|
|
850
|
-
returning: boolean;
|
|
851
|
-
}
|
|
852
|
-
) => {
|
|
853
|
-
const { where, returning } = options;
|
|
854
|
-
let query = `DELETE FROM ${TableName}`;
|
|
855
|
-
|
|
856
|
-
query += this.BuildWhereClause(where);
|
|
857
|
-
|
|
858
|
-
if (returning) {
|
|
859
|
-
query += " RETURNING *";
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
return { sql: query, args: [] };
|
|
863
|
-
};
|
|
864
|
-
|
|
865
|
-
// Helper to handle the deep recursion
|
|
866
|
-
private parseRecursive = (value: any): any => {
|
|
867
|
-
// 1. Handle Null/Undefined
|
|
868
|
-
if (value === null || value === undefined) {
|
|
869
|
-
return value;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
// 2. Handle Dates (return as is, don't try to recurse properties)
|
|
873
|
-
if (value instanceof Date) {
|
|
874
|
-
return value;
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
// 3. Handle Arrays (recurse over every item)
|
|
878
|
-
if (Array.isArray(value)) {
|
|
879
|
-
return value.map((item) => this.parseRecursive(item));
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
// 4. Handle Objects (DuckDB structs, maps, or generic objects)
|
|
883
|
-
if (typeof value === "object") {
|
|
884
|
-
// DuckDB specific: If it has an 'entries' property, unwrap it and recurse
|
|
885
|
-
// @ts-ignore
|
|
886
|
-
if (value.entries !== undefined) {
|
|
887
|
-
// @ts-ignore
|
|
888
|
-
return this.parseRecursive(value.entries);
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
// DuckDB specific: If it has an 'items' property (lists), unwrap it and recurse
|
|
892
|
-
// @ts-ignore
|
|
893
|
-
if (value.items !== undefined) {
|
|
894
|
-
// @ts-ignore
|
|
895
|
-
return this.parseRecursive(value.items);
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
// Standard Object: recurse over all keys
|
|
899
|
-
const parsedObj: Record<string, any> = {};
|
|
900
|
-
for (const [k, v] of Object.entries(value)) {
|
|
901
|
-
parsedObj[k] = this.parseRecursive(v);
|
|
902
|
-
}
|
|
903
|
-
return parsedObj;
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
// 5. Primitives (string, number, boolean)
|
|
907
|
-
return value;
|
|
908
|
-
};
|
|
909
|
-
|
|
910
|
-
private FormatOut = (data: Record<string, DuckDBValue>[]) => {
|
|
911
|
-
return data.map((row) => {
|
|
912
|
-
const formatted: Record<string, any> = {};
|
|
913
|
-
|
|
914
|
-
for (const [key, value] of Object.entries(row)) {
|
|
915
|
-
if (key.startsWith("$") && value === null) {
|
|
916
|
-
formatted[key] = [];
|
|
917
|
-
continue;
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
// 2. Recursively parse everything else
|
|
921
|
-
formatted[key] = this.parseRecursive(value);
|
|
922
|
-
}
|
|
923
|
-
return formatted;
|
|
924
|
-
});
|
|
925
|
-
};
|
|
926
|
-
}
|