@bluecopa/core 0.1.58 → 0.1.59
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/README.md +65 -0
- package/dist/config.d.ts +2 -0
- package/dist/index.es.js +360 -68
- package/dist/index.es.js.map +1 -1
- package/dist/input-table-db/aggregateUtils.d.ts +78 -0
- package/dist/input-table-db/collectionRef.d.ts +23 -1
- package/dist/input-table-db/index.d.ts +1 -1
- package/dist/input-table-db/queryBuilder.d.ts +12 -7
- package/dist/input-table-db/types.d.ts +34 -0
- package/dist/input-table-db/utils.d.ts +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@ The core package provides essential API utilities and functions for data managem
|
|
|
24
24
|
- [Quick Start](#quick-start)
|
|
25
25
|
- [CRUD](#crud)
|
|
26
26
|
- [Query Operators](#query-operators)
|
|
27
|
+
- [Aggregate Queries](#aggregate-queries)
|
|
27
28
|
- [Framework Integration](#framework-integration)
|
|
28
29
|
- [WebSocket Provider (optional)](#websocket-provider-optional)
|
|
29
30
|
- [Cleanup](#cleanup)
|
|
@@ -248,6 +249,70 @@ const unsub = copaInputTableDb.collection("invoices").count((n) => console.log(n
|
|
|
248
249
|
| `in` | in array |
|
|
249
250
|
| `not-in` | not in array |
|
|
250
251
|
|
|
252
|
+
### Aggregate Queries
|
|
253
|
+
|
|
254
|
+
Compute server-side aggregates (`sum`, `avg`, `count`, `min`, `max`) without fetching all rows. Combines with `where()`, `limit()`, and `skip()` filters.
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
// Column aggregates
|
|
258
|
+
const result = await copaInputTableDb
|
|
259
|
+
.collection("invoices")
|
|
260
|
+
.where("status", "==", "active")
|
|
261
|
+
.aggregate({ amount: ["sum", "avg"], price: ["min", "max"] });
|
|
262
|
+
// => { amount: { sum: 1234.56, avg: 123.45 }, price: { min: 10, max: 999 } }
|
|
263
|
+
|
|
264
|
+
// Row count
|
|
265
|
+
const result = await copaInputTableDb
|
|
266
|
+
.collection("invoices")
|
|
267
|
+
.aggregate({ _count: true });
|
|
268
|
+
// => { _count: 42 }
|
|
269
|
+
|
|
270
|
+
// Column count (non-null values) + row count
|
|
271
|
+
const result = await copaInputTableDb
|
|
272
|
+
.collection("invoices")
|
|
273
|
+
.aggregate({ name: ["count"], _count: true });
|
|
274
|
+
// => { name: { count: 38 }, _count: 42 }
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
`.aggregate()` is a **terminal method** — it bypasses local RxDB and hits PostgREST directly. Errors throw `InputTableError`. An empty spec `{}` returns `{}` without calling the API.
|
|
278
|
+
|
|
279
|
+
### Grouped Aggregates
|
|
280
|
+
|
|
281
|
+
Add a `{ groupBy: [...columns] }` second argument to get per-group breakdowns. Returns an array instead of a single object.
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
// Sum per status group
|
|
285
|
+
const rows = await copaInputTableDb
|
|
286
|
+
.collection("invoices")
|
|
287
|
+
.aggregate({ amount: ["sum"] }, { groupBy: ["status"] });
|
|
288
|
+
// => [{ status: "active", amount: { sum: 1234 } }, { status: "draft", amount: { sum: 100 } }]
|
|
289
|
+
|
|
290
|
+
// Multi-column groupBy with filters and ordering
|
|
291
|
+
const rows = await copaInputTableDb
|
|
292
|
+
.collection("invoices")
|
|
293
|
+
.where("year", "==", 2024)
|
|
294
|
+
.orderBy("order_date", "asc")
|
|
295
|
+
.aggregate({ amount: ["sum", "avg"] }, { groupBy: ["order_date", "status"] });
|
|
296
|
+
|
|
297
|
+
// Count per group
|
|
298
|
+
const rows = await copaInputTableDb
|
|
299
|
+
.collection("invoices")
|
|
300
|
+
.aggregate({ _count: true }, { groupBy: ["status"] });
|
|
301
|
+
// => [{ status: "active", _count: 10 }, { status: "draft", _count: 4 }]
|
|
302
|
+
|
|
303
|
+
// Distinct values (no aggregate functions)
|
|
304
|
+
const rows = await copaInputTableDb
|
|
305
|
+
.collection("invoices")
|
|
306
|
+
.aggregate({}, { groupBy: ["status"] });
|
|
307
|
+
// => [{ status: "active" }, { status: "draft" }]
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**Notes:**
|
|
311
|
+
- `orderBy()` is forwarded to PostgREST when `groupBy` is present (ignored otherwise)
|
|
312
|
+
- `limit()`/`skip()` apply to the number of _groups_, not input rows
|
|
313
|
+
- A column cannot appear in both the aggregate spec and `groupBy` — throws `InputTableError`
|
|
314
|
+
- Empty `groupBy: []` behaves like no groupBy — returns a single object
|
|
315
|
+
|
|
251
316
|
### Framework Integration
|
|
252
317
|
|
|
253
318
|
**Svelte 5**
|
package/dist/config.d.ts
CHANGED
package/dist/index.es.js
CHANGED
|
@@ -37,6 +37,8 @@ class ConfigSingleton {
|
|
|
37
37
|
workspaceId: "",
|
|
38
38
|
userId: "",
|
|
39
39
|
solutionId: void 0,
|
|
40
|
+
solutionBranch: void 0,
|
|
41
|
+
solutionBranchType: void 0,
|
|
40
42
|
websocketProvider: void 0
|
|
41
43
|
};
|
|
42
44
|
}
|
|
@@ -54,6 +56,22 @@ const createApiClient = () => {
|
|
|
54
56
|
// Increased to 120 seconds for audit log queries
|
|
55
57
|
headers: {
|
|
56
58
|
"Content-Type": "application/json"
|
|
59
|
+
},
|
|
60
|
+
// PostgREST requires repeated params for multi-value filters: amount=gt.10&amount=lt.100
|
|
61
|
+
// Axios's default qs serializer uses indices format (amount[0]=gt.10) which PostgREST
|
|
62
|
+
// does not understand. This custom serializer produces the correct repeated-param format.
|
|
63
|
+
paramsSerializer: (params) => {
|
|
64
|
+
const parts = [];
|
|
65
|
+
for (const [key, value] of Object.entries(params)) {
|
|
66
|
+
if (Array.isArray(value)) {
|
|
67
|
+
for (const v of value) {
|
|
68
|
+
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(v))}`);
|
|
69
|
+
}
|
|
70
|
+
} else if (value !== null && value !== void 0) {
|
|
71
|
+
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return parts.join("&");
|
|
57
75
|
}
|
|
58
76
|
});
|
|
59
77
|
client.interceptors.request.use(
|
|
@@ -71,11 +89,6 @@ const createApiClient = () => {
|
|
|
71
89
|
if (copaConfig.solutionId && config.headers) {
|
|
72
90
|
config.headers["x-bluecopa-solution-id"] = copaConfig.solutionId;
|
|
73
91
|
}
|
|
74
|
-
console.log("API Request Config:", {
|
|
75
|
-
baseURL: config.baseURL,
|
|
76
|
-
hasToken: !!copaConfig.accessToken,
|
|
77
|
-
hasWorkspaceId: !!copaConfig.workspaceId
|
|
78
|
-
});
|
|
79
92
|
return config;
|
|
80
93
|
},
|
|
81
94
|
(error) => {
|
|
@@ -12260,6 +12273,75 @@ var InputTableColumnType = /* @__PURE__ */ ((InputTableColumnType2) => {
|
|
|
12260
12273
|
InputTableColumnType2["JSONB"] = "jsonb";
|
|
12261
12274
|
return InputTableColumnType2;
|
|
12262
12275
|
})(InputTableColumnType || {});
|
|
12276
|
+
const IDENTIFIER_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
12277
|
+
const SOLUTION_BRANCH_COOKIE_NAME = "x-bluecopa-solution-branch";
|
|
12278
|
+
const SOLUTION_BRANCH_TYPE_COOKIE_NAME = "x-bluecopa-solution-branch-type";
|
|
12279
|
+
function readCookie(name) {
|
|
12280
|
+
try {
|
|
12281
|
+
const cookie = document.cookie.split(";").map((c) => c.trim()).find((c) => c.startsWith(`${name}=`));
|
|
12282
|
+
return cookie ? decodeURIComponent(cookie.split("=").slice(1).join("=")) : null;
|
|
12283
|
+
} catch {
|
|
12284
|
+
return null;
|
|
12285
|
+
}
|
|
12286
|
+
}
|
|
12287
|
+
function getSolutionBranchHeaders() {
|
|
12288
|
+
const config = getConfig();
|
|
12289
|
+
const branch = config.solutionBranch ?? readCookie(SOLUTION_BRANCH_COOKIE_NAME);
|
|
12290
|
+
const branchType = config.solutionBranchType ?? readCookie(SOLUTION_BRANCH_TYPE_COOKIE_NAME);
|
|
12291
|
+
const headers = {};
|
|
12292
|
+
if (branch) headers["x-bluecopa-solution-branch"] = branch;
|
|
12293
|
+
if (branchType) headers["x-bluecopa-solution-branch-type"] = branchType;
|
|
12294
|
+
return headers;
|
|
12295
|
+
}
|
|
12296
|
+
const ISO_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;
|
|
12297
|
+
class InputTableError extends Error {
|
|
12298
|
+
constructor(message, status) {
|
|
12299
|
+
super(message);
|
|
12300
|
+
this.status = status;
|
|
12301
|
+
this.name = "InputTableError";
|
|
12302
|
+
}
|
|
12303
|
+
}
|
|
12304
|
+
function validateIdentifier(value, label) {
|
|
12305
|
+
const trimmed = value.trim();
|
|
12306
|
+
if (!trimmed || !IDENTIFIER_PATTERN.test(trimmed)) {
|
|
12307
|
+
throw new InputTableError(`Invalid ${label}: "${value}"`, 400);
|
|
12308
|
+
}
|
|
12309
|
+
return trimmed;
|
|
12310
|
+
}
|
|
12311
|
+
function validateCheckpointTimestamp(value) {
|
|
12312
|
+
if (!ISO_TIMESTAMP_PATTERN.test(value)) {
|
|
12313
|
+
throw new InputTableError(`Invalid checkpoint timestamp: "${value}"`, 400);
|
|
12314
|
+
}
|
|
12315
|
+
return value;
|
|
12316
|
+
}
|
|
12317
|
+
function validateCheckpointId(value) {
|
|
12318
|
+
return validateIdentifier(value, "checkpoint id");
|
|
12319
|
+
}
|
|
12320
|
+
function stripNullsAndCoercePk(row, primaryKeyField) {
|
|
12321
|
+
const doc = {};
|
|
12322
|
+
for (const [key, value] of Object.entries(row)) {
|
|
12323
|
+
if (value === null) continue;
|
|
12324
|
+
doc[key] = value;
|
|
12325
|
+
}
|
|
12326
|
+
doc[primaryKeyField] = String(row[primaryKeyField]);
|
|
12327
|
+
return doc;
|
|
12328
|
+
}
|
|
12329
|
+
function deferSubscription(resolve, callback, fallback) {
|
|
12330
|
+
let innerUnsub = null;
|
|
12331
|
+
let cancelled = false;
|
|
12332
|
+
resolve().then((subscribeFn) => {
|
|
12333
|
+
if (!cancelled) {
|
|
12334
|
+
innerUnsub = subscribeFn(callback);
|
|
12335
|
+
}
|
|
12336
|
+
}).catch((err) => {
|
|
12337
|
+
console.error("[copaInputTableDb]", err);
|
|
12338
|
+
if (!cancelled) callback(fallback);
|
|
12339
|
+
});
|
|
12340
|
+
return () => {
|
|
12341
|
+
cancelled = true;
|
|
12342
|
+
innerUnsub == null ? void 0 : innerUnsub();
|
|
12343
|
+
};
|
|
12344
|
+
}
|
|
12263
12345
|
const OP_MAP = {
|
|
12264
12346
|
"==": "$eq",
|
|
12265
12347
|
"!=": "$ne",
|
|
@@ -12271,8 +12353,9 @@ const OP_MAP = {
|
|
|
12271
12353
|
"not-in": "$nin"
|
|
12272
12354
|
};
|
|
12273
12355
|
class QueryBuilderImpl {
|
|
12274
|
-
constructor(collection, initialWhere, initialOrder, initialLimit, initialSkip) {
|
|
12356
|
+
constructor(collection, initialWhere, initialOrder, initialLimit, initialSkip, aggregateExecutor) {
|
|
12275
12357
|
this.collection = collection;
|
|
12358
|
+
this.aggregateExecutor = aggregateExecutor;
|
|
12276
12359
|
this.wheres = [];
|
|
12277
12360
|
this.orders = [];
|
|
12278
12361
|
if (initialWhere) this.wheres.push(initialWhere);
|
|
@@ -12308,6 +12391,22 @@ class QueryBuilderImpl {
|
|
|
12308
12391
|
const docs = await query.exec();
|
|
12309
12392
|
return docs.map((d) => d.toJSON());
|
|
12310
12393
|
}
|
|
12394
|
+
async aggregate(spec, options) {
|
|
12395
|
+
if (!this.aggregateExecutor) {
|
|
12396
|
+
throw new InputTableError(
|
|
12397
|
+
"[copaInputTableDb] aggregate() is not available on this QueryBuilder instance",
|
|
12398
|
+
500
|
|
12399
|
+
);
|
|
12400
|
+
}
|
|
12401
|
+
return this.aggregateExecutor(
|
|
12402
|
+
this.wheres,
|
|
12403
|
+
this.orders,
|
|
12404
|
+
this.limitCount,
|
|
12405
|
+
this.skipCount,
|
|
12406
|
+
spec,
|
|
12407
|
+
options == null ? void 0 : options.groupBy
|
|
12408
|
+
);
|
|
12409
|
+
}
|
|
12311
12410
|
buildQuery() {
|
|
12312
12411
|
const selector = {};
|
|
12313
12412
|
for (const { field, op, value } of this.wheres) {
|
|
@@ -12333,56 +12432,6 @@ const MAX_COLLECTIONS = 14;
|
|
|
12333
12432
|
const REPLICATION_RETRY_TIME = 5e3;
|
|
12334
12433
|
const PULL_BATCH_SIZE = 100;
|
|
12335
12434
|
const PUSH_BATCH_SIZE = 10;
|
|
12336
|
-
const IDENTIFIER_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
12337
|
-
const ISO_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;
|
|
12338
|
-
class InputTableError extends Error {
|
|
12339
|
-
constructor(message, status) {
|
|
12340
|
-
super(message);
|
|
12341
|
-
this.status = status;
|
|
12342
|
-
this.name = "InputTableError";
|
|
12343
|
-
}
|
|
12344
|
-
}
|
|
12345
|
-
function validateIdentifier(value, label) {
|
|
12346
|
-
const trimmed = value.trim();
|
|
12347
|
-
if (!trimmed || !IDENTIFIER_PATTERN.test(trimmed)) {
|
|
12348
|
-
throw new InputTableError(`Invalid ${label}: "${value}"`, 400);
|
|
12349
|
-
}
|
|
12350
|
-
return trimmed;
|
|
12351
|
-
}
|
|
12352
|
-
function validateCheckpointTimestamp(value) {
|
|
12353
|
-
if (!ISO_TIMESTAMP_PATTERN.test(value)) {
|
|
12354
|
-
throw new InputTableError(`Invalid checkpoint timestamp: "${value}"`, 400);
|
|
12355
|
-
}
|
|
12356
|
-
return value;
|
|
12357
|
-
}
|
|
12358
|
-
function validateCheckpointId(value) {
|
|
12359
|
-
return validateIdentifier(value, "checkpoint id");
|
|
12360
|
-
}
|
|
12361
|
-
function stripNullsAndCoercePk(row, primaryKeyField) {
|
|
12362
|
-
const doc = {};
|
|
12363
|
-
for (const [key, value] of Object.entries(row)) {
|
|
12364
|
-
if (value === null) continue;
|
|
12365
|
-
doc[key] = value;
|
|
12366
|
-
}
|
|
12367
|
-
doc[primaryKeyField] = String(row[primaryKeyField]);
|
|
12368
|
-
return doc;
|
|
12369
|
-
}
|
|
12370
|
-
function deferSubscription(resolve, callback, fallback) {
|
|
12371
|
-
let innerUnsub = null;
|
|
12372
|
-
let cancelled = false;
|
|
12373
|
-
resolve().then((subscribeFn) => {
|
|
12374
|
-
if (!cancelled) {
|
|
12375
|
-
innerUnsub = subscribeFn(callback);
|
|
12376
|
-
}
|
|
12377
|
-
}).catch((err) => {
|
|
12378
|
-
console.error("[copaInputTableDb]", err);
|
|
12379
|
-
if (!cancelled) callback(fallback);
|
|
12380
|
-
});
|
|
12381
|
-
return () => {
|
|
12382
|
-
cancelled = true;
|
|
12383
|
-
innerUnsub == null ? void 0 : innerUnsub();
|
|
12384
|
-
};
|
|
12385
|
-
}
|
|
12386
12435
|
class DocRefImpl {
|
|
12387
12436
|
constructor(collection, id, solutionId, tableName, primaryKeyField, websocketProvider) {
|
|
12388
12437
|
this.collection = collection;
|
|
@@ -12422,6 +12471,168 @@ class DocRefImpl {
|
|
|
12422
12471
|
return () => rxSub.unsubscribe();
|
|
12423
12472
|
}
|
|
12424
12473
|
}
|
|
12474
|
+
const POSTGREST_OP_MAP = {
|
|
12475
|
+
"==": "eq",
|
|
12476
|
+
"!=": "neq",
|
|
12477
|
+
"<": "lt",
|
|
12478
|
+
"<=": "lte",
|
|
12479
|
+
">": "gt",
|
|
12480
|
+
">=": "gte",
|
|
12481
|
+
in: "in",
|
|
12482
|
+
"not-in": "not.in"
|
|
12483
|
+
};
|
|
12484
|
+
const VALID_AGGREGATE_FUNCTIONS = /* @__PURE__ */ new Set([
|
|
12485
|
+
"sum",
|
|
12486
|
+
"avg",
|
|
12487
|
+
"count",
|
|
12488
|
+
"min",
|
|
12489
|
+
"max"
|
|
12490
|
+
]);
|
|
12491
|
+
function validateAggregateSpec(spec) {
|
|
12492
|
+
for (const [key, value] of Object.entries(spec)) {
|
|
12493
|
+
if (key === "_count") {
|
|
12494
|
+
if (value !== true) {
|
|
12495
|
+
throw new InputTableError(
|
|
12496
|
+
`"_count" must be exactly \`true\`, got: ${JSON.stringify(value)}`,
|
|
12497
|
+
400
|
|
12498
|
+
);
|
|
12499
|
+
}
|
|
12500
|
+
continue;
|
|
12501
|
+
}
|
|
12502
|
+
validateIdentifier(key, `aggregate column "${key}"`);
|
|
12503
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
12504
|
+
throw new InputTableError(
|
|
12505
|
+
`Aggregate spec column "${key}" must have a non-empty array of functions`,
|
|
12506
|
+
400
|
|
12507
|
+
);
|
|
12508
|
+
}
|
|
12509
|
+
for (const fn of value) {
|
|
12510
|
+
if (!VALID_AGGREGATE_FUNCTIONS.has(fn)) {
|
|
12511
|
+
throw new InputTableError(
|
|
12512
|
+
`Unknown aggregate function "${fn}" for column "${key}". Allowed: sum, avg, count, min, max`,
|
|
12513
|
+
400
|
|
12514
|
+
);
|
|
12515
|
+
}
|
|
12516
|
+
}
|
|
12517
|
+
}
|
|
12518
|
+
}
|
|
12519
|
+
function validateGroupBySpec(groupBy, spec) {
|
|
12520
|
+
const specKeys = new Set(Object.keys(spec).filter((k) => k !== "_count"));
|
|
12521
|
+
if (specKeys.size === 0 && !spec._count) {
|
|
12522
|
+
throw new InputTableError(
|
|
12523
|
+
"groupBy requires at least one aggregate function or `_count: true` in the spec",
|
|
12524
|
+
400
|
|
12525
|
+
);
|
|
12526
|
+
}
|
|
12527
|
+
for (const col of groupBy) {
|
|
12528
|
+
validateIdentifier(col, `groupBy column "${col}"`);
|
|
12529
|
+
if (specKeys.has(col)) {
|
|
12530
|
+
throw new InputTableError(
|
|
12531
|
+
`groupBy column "${col}" overlaps with aggregate spec key. Use a different column name or separate the queries.`,
|
|
12532
|
+
400
|
|
12533
|
+
);
|
|
12534
|
+
}
|
|
12535
|
+
}
|
|
12536
|
+
}
|
|
12537
|
+
function buildAggregateSelect(spec, groupBy) {
|
|
12538
|
+
const parts = [];
|
|
12539
|
+
if (groupBy && groupBy.length > 0) {
|
|
12540
|
+
for (const col of groupBy) {
|
|
12541
|
+
parts.push(col);
|
|
12542
|
+
}
|
|
12543
|
+
}
|
|
12544
|
+
for (const [key, value] of Object.entries(spec)) {
|
|
12545
|
+
if (key === "_count") continue;
|
|
12546
|
+
for (const fn of value) {
|
|
12547
|
+
parts.push(`${key}_${fn}:${key}.${fn}()`);
|
|
12548
|
+
}
|
|
12549
|
+
}
|
|
12550
|
+
if (spec._count) {
|
|
12551
|
+
parts.push("count()");
|
|
12552
|
+
}
|
|
12553
|
+
return parts.join(",");
|
|
12554
|
+
}
|
|
12555
|
+
function buildPostgrestOrder(orders) {
|
|
12556
|
+
for (const { field } of orders) {
|
|
12557
|
+
validateIdentifier(field, `order field "${field}"`);
|
|
12558
|
+
}
|
|
12559
|
+
return orders.map(({ field, direction }) => `${field}.${direction}`).join(",");
|
|
12560
|
+
}
|
|
12561
|
+
const RESERVED_PARAMS = /* @__PURE__ */ new Set(["select", "order", "limit", "offset", "on_conflict", "columns", "and", "or", "not"]);
|
|
12562
|
+
function buildPostgrestFilters(wheres) {
|
|
12563
|
+
const params = {};
|
|
12564
|
+
for (const { field, op, value } of wheres) {
|
|
12565
|
+
validateIdentifier(field, `where field "${field}"`);
|
|
12566
|
+
if (RESERVED_PARAMS.has(field)) {
|
|
12567
|
+
throw new InputTableError(
|
|
12568
|
+
`Cannot filter on reserved PostgREST param name "${field}"`,
|
|
12569
|
+
400
|
|
12570
|
+
);
|
|
12571
|
+
}
|
|
12572
|
+
const pgOp = POSTGREST_OP_MAP[op];
|
|
12573
|
+
let filterStr;
|
|
12574
|
+
if (op === "in" || op === "not-in") {
|
|
12575
|
+
const arr = Array.isArray(value) ? value : [value];
|
|
12576
|
+
filterStr = `${pgOp}.(${arr.map(quotePostgrestValue).join(",")})`;
|
|
12577
|
+
} else {
|
|
12578
|
+
filterStr = `${pgOp}.${quotePostgrestValue(value)}`;
|
|
12579
|
+
}
|
|
12580
|
+
const existing = params[field];
|
|
12581
|
+
if (existing === void 0) {
|
|
12582
|
+
params[field] = filterStr;
|
|
12583
|
+
} else if (Array.isArray(existing)) {
|
|
12584
|
+
existing.push(filterStr);
|
|
12585
|
+
} else {
|
|
12586
|
+
params[field] = [existing, filterStr];
|
|
12587
|
+
}
|
|
12588
|
+
}
|
|
12589
|
+
return params;
|
|
12590
|
+
}
|
|
12591
|
+
function quotePostgrestValue(value) {
|
|
12592
|
+
if (value === null || value === void 0) {
|
|
12593
|
+
throw new InputTableError("Filter value must not be null or undefined", 400);
|
|
12594
|
+
}
|
|
12595
|
+
if (typeof value === "object") {
|
|
12596
|
+
throw new InputTableError(
|
|
12597
|
+
`Filter value must be a primitive, got: ${Array.isArray(value) ? "array" : "object"}`,
|
|
12598
|
+
400
|
|
12599
|
+
);
|
|
12600
|
+
}
|
|
12601
|
+
const str = String(value);
|
|
12602
|
+
if (/[,.:()\\"']/.test(str)) {
|
|
12603
|
+
const escaped = str.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
12604
|
+
return `"${escaped}"`;
|
|
12605
|
+
}
|
|
12606
|
+
return str;
|
|
12607
|
+
}
|
|
12608
|
+
function parseRowAggregates(spec, row) {
|
|
12609
|
+
const result = {};
|
|
12610
|
+
for (const [key, value] of Object.entries(spec)) {
|
|
12611
|
+
if (key === "_count") continue;
|
|
12612
|
+
const nested = {};
|
|
12613
|
+
for (const fn of value) {
|
|
12614
|
+
const alias = `${key}_${fn}`;
|
|
12615
|
+
nested[fn] = alias in row ? row[alias] : null;
|
|
12616
|
+
}
|
|
12617
|
+
result[key] = nested;
|
|
12618
|
+
}
|
|
12619
|
+
if (spec._count) {
|
|
12620
|
+
result["_count"] = row["count"] ?? 0;
|
|
12621
|
+
}
|
|
12622
|
+
return result;
|
|
12623
|
+
}
|
|
12624
|
+
function parseAggregateResponse(spec, data) {
|
|
12625
|
+
return parseRowAggregates(spec, data[0] ?? {});
|
|
12626
|
+
}
|
|
12627
|
+
function parseGroupedAggregateResponse(spec, groupBy, data) {
|
|
12628
|
+
return data.map((row) => {
|
|
12629
|
+
const result = parseRowAggregates(spec, row);
|
|
12630
|
+
for (const col of groupBy) {
|
|
12631
|
+
result[col] = row[col];
|
|
12632
|
+
}
|
|
12633
|
+
return result;
|
|
12634
|
+
});
|
|
12635
|
+
}
|
|
12425
12636
|
class CollectionRefImpl {
|
|
12426
12637
|
constructor(collection, solutionId, tableName, primaryKeyField, pkColumn, replicationState, getWebsocketProvider) {
|
|
12427
12638
|
this.collection = collection;
|
|
@@ -12432,17 +12643,27 @@ class CollectionRefImpl {
|
|
|
12432
12643
|
this.replicationState = replicationState;
|
|
12433
12644
|
this.getWebsocketProvider = getWebsocketProvider;
|
|
12434
12645
|
}
|
|
12646
|
+
makeQueryBuilder(initialWhere, initialOrder, initialLimit, initialSkip) {
|
|
12647
|
+
return new QueryBuilderImpl(
|
|
12648
|
+
this.collection,
|
|
12649
|
+
initialWhere,
|
|
12650
|
+
initialOrder,
|
|
12651
|
+
initialLimit,
|
|
12652
|
+
initialSkip,
|
|
12653
|
+
(wheres, orders, limit, skip, spec, groupBy) => this.executeAggregate(wheres, orders, limit, skip, spec, groupBy)
|
|
12654
|
+
);
|
|
12655
|
+
}
|
|
12435
12656
|
where(field, op, value) {
|
|
12436
|
-
return
|
|
12657
|
+
return this.makeQueryBuilder({ field, op, value });
|
|
12437
12658
|
}
|
|
12438
12659
|
orderBy(field, direction = "asc") {
|
|
12439
|
-
return
|
|
12660
|
+
return this.makeQueryBuilder(void 0, { field, direction });
|
|
12440
12661
|
}
|
|
12441
12662
|
limit(count) {
|
|
12442
|
-
return
|
|
12663
|
+
return this.makeQueryBuilder(void 0, void 0, count);
|
|
12443
12664
|
}
|
|
12444
12665
|
skip(count) {
|
|
12445
|
-
return
|
|
12666
|
+
return this.makeQueryBuilder(void 0, void 0, void 0, count);
|
|
12446
12667
|
}
|
|
12447
12668
|
doc(id) {
|
|
12448
12669
|
return new DocRefImpl(
|
|
@@ -12498,6 +12719,58 @@ class CollectionRefImpl {
|
|
|
12498
12719
|
});
|
|
12499
12720
|
return () => rxSub.unsubscribe();
|
|
12500
12721
|
}
|
|
12722
|
+
async aggregate(spec, options) {
|
|
12723
|
+
return this.executeAggregate([], [], void 0, void 0, spec, options == null ? void 0 : options.groupBy);
|
|
12724
|
+
}
|
|
12725
|
+
async executeAggregate(wheres, orders, limit, skip, spec, groupBy) {
|
|
12726
|
+
var _a, _b, _c;
|
|
12727
|
+
validateAggregateSpec(spec);
|
|
12728
|
+
const activeGroupBy = groupBy && groupBy.length > 0 ? groupBy : void 0;
|
|
12729
|
+
if (activeGroupBy) {
|
|
12730
|
+
validateGroupBySpec(activeGroupBy, spec);
|
|
12731
|
+
}
|
|
12732
|
+
const selectParam = buildAggregateSelect(spec, activeGroupBy);
|
|
12733
|
+
if (!selectParam) {
|
|
12734
|
+
return activeGroupBy ? [] : {};
|
|
12735
|
+
}
|
|
12736
|
+
const filterParams = buildPostgrestFilters(wheres);
|
|
12737
|
+
const params = {
|
|
12738
|
+
select: selectParam,
|
|
12739
|
+
...filterParams
|
|
12740
|
+
};
|
|
12741
|
+
if (limit !== void 0) params["limit"] = limit;
|
|
12742
|
+
if (skip !== void 0) params["offset"] = skip;
|
|
12743
|
+
if (activeGroupBy && orders.length > 0) {
|
|
12744
|
+
const groupBySet = new Set(activeGroupBy);
|
|
12745
|
+
for (const { field } of orders) {
|
|
12746
|
+
if (!groupBySet.has(field)) {
|
|
12747
|
+
throw new InputTableError(
|
|
12748
|
+
`orderBy field "${field}" must be one of the groupBy columns: [${activeGroupBy.join(", ")}]`,
|
|
12749
|
+
400
|
|
12750
|
+
);
|
|
12751
|
+
}
|
|
12752
|
+
}
|
|
12753
|
+
params["order"] = buildPostgrestOrder(orders);
|
|
12754
|
+
}
|
|
12755
|
+
let response;
|
|
12756
|
+
try {
|
|
12757
|
+
response = await apiClient.get(
|
|
12758
|
+
`/input-table-v2/${this.solutionId}/rows/${this.tableName}`,
|
|
12759
|
+
{ params, headers: getSolutionBranchHeaders() }
|
|
12760
|
+
);
|
|
12761
|
+
} catch (err) {
|
|
12762
|
+
const axiosErr = err;
|
|
12763
|
+
throw new InputTableError(
|
|
12764
|
+
((_b = (_a = axiosErr.response) == null ? void 0 : _a.data) == null ? void 0 : _b.message) ?? axiosErr.message ?? "Aggregate query failed",
|
|
12765
|
+
((_c = axiosErr.response) == null ? void 0 : _c.status) ?? 500
|
|
12766
|
+
);
|
|
12767
|
+
}
|
|
12768
|
+
const data = Array.isArray(response.data) ? response.data : [response.data];
|
|
12769
|
+
if (activeGroupBy) {
|
|
12770
|
+
return parseGroupedAggregateResponse(spec, activeGroupBy, data);
|
|
12771
|
+
}
|
|
12772
|
+
return parseAggregateResponse(spec, data);
|
|
12773
|
+
}
|
|
12501
12774
|
}
|
|
12502
12775
|
let dbPromise = null;
|
|
12503
12776
|
function getInputTableDatabase() {
|
|
@@ -12850,18 +13123,22 @@ async function evictLeastRecentlyUsed() {
|
|
|
12850
13123
|
}
|
|
12851
13124
|
}
|
|
12852
13125
|
const SOLUTION_COOKIE_NAME = "x-bluecopa-solution-id";
|
|
13126
|
+
function getCookieValue(name) {
|
|
13127
|
+
try {
|
|
13128
|
+
const cookie = document.cookie.split(";").map((c) => c.trim()).find((c) => c.startsWith(`${name}=`));
|
|
13129
|
+
return cookie ? decodeURIComponent(cookie.split("=").slice(1).join("=")) : null;
|
|
13130
|
+
} catch {
|
|
13131
|
+
return null;
|
|
13132
|
+
}
|
|
13133
|
+
}
|
|
12853
13134
|
function getSolutionId() {
|
|
12854
13135
|
const config = getConfig();
|
|
12855
13136
|
if (config.solutionId) {
|
|
12856
13137
|
return validateIdentifier(config.solutionId, "solutionId");
|
|
12857
13138
|
}
|
|
12858
|
-
|
|
12859
|
-
|
|
12860
|
-
|
|
12861
|
-
const value = decodeURIComponent(cookie.split("=").slice(1).join("="));
|
|
12862
|
-
return validateIdentifier(value, "solutionId (cookie)");
|
|
12863
|
-
}
|
|
12864
|
-
} catch {
|
|
13139
|
+
const cookieValue = getCookieValue(SOLUTION_COOKIE_NAME);
|
|
13140
|
+
if (cookieValue) {
|
|
13141
|
+
return validateIdentifier(cookieValue, "solutionId (cookie)");
|
|
12865
13142
|
}
|
|
12866
13143
|
throw new InputTableError(
|
|
12867
13144
|
"[copaInputTableDb] No solutionId configured. Set via copaSetConfig({ solutionId }) or cookie.",
|
|
@@ -12933,7 +13210,8 @@ const _InputTableDBImpl = class _InputTableDBImpl {
|
|
|
12933
13210
|
if (this.schemaPending.has(solutionId)) {
|
|
12934
13211
|
return this.schemaPending.get(solutionId);
|
|
12935
13212
|
}
|
|
12936
|
-
const
|
|
13213
|
+
const headers = getSolutionBranchHeaders();
|
|
13214
|
+
const pending = apiClient.get(`/input-table-v2/${solutionId}/tables`, { headers }).then((res) => {
|
|
12937
13215
|
const tables = res.data ?? [];
|
|
12938
13216
|
this.schemaCache.set(solutionId, { tables, fetchedAt: Date.now() });
|
|
12939
13217
|
this.schemaPending.delete(solutionId);
|
|
@@ -13013,6 +13291,13 @@ class LazyCollectionRef {
|
|
|
13013
13291
|
0
|
|
13014
13292
|
);
|
|
13015
13293
|
}
|
|
13294
|
+
async aggregate(spec, options) {
|
|
13295
|
+
const ref = await this.getRef();
|
|
13296
|
+
if (options) {
|
|
13297
|
+
return ref.aggregate(spec, options);
|
|
13298
|
+
}
|
|
13299
|
+
return ref.aggregate(spec);
|
|
13300
|
+
}
|
|
13016
13301
|
}
|
|
13017
13302
|
class LazyQueryBuilder {
|
|
13018
13303
|
constructor(resolveBase) {
|
|
@@ -13058,6 +13343,13 @@ class LazyQueryBuilder {
|
|
|
13058
13343
|
return [];
|
|
13059
13344
|
}
|
|
13060
13345
|
}
|
|
13346
|
+
async aggregate(spec, options) {
|
|
13347
|
+
const qb = await this.resolveWithOps();
|
|
13348
|
+
if (options) {
|
|
13349
|
+
return qb.aggregate(spec, options);
|
|
13350
|
+
}
|
|
13351
|
+
return qb.aggregate(spec);
|
|
13352
|
+
}
|
|
13061
13353
|
}
|
|
13062
13354
|
class LazyDocRef {
|
|
13063
13355
|
constructor(id, resolve) {
|