@rebasepro/server-postgres 0.19.2-canary.gef769df → 0.20.1-canary.g4d882ca
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.es.js +71 -12
- package/dist/index.es.js.map +1 -1
- package/dist/services/FetchService.d.ts +25 -0
- package/dist/services/row-pipeline.d.ts +12 -3
- package/package.json +7 -7
package/dist/index.es.js
CHANGED
|
@@ -2537,12 +2537,49 @@ function coerceDeclaredNumber(value, property) {
|
|
|
2537
2537
|
const parsed = parseFloat(value);
|
|
2538
2538
|
return isNaN(parsed) ? null : parsed;
|
|
2539
2539
|
}
|
|
2540
|
-
/**
|
|
2541
|
-
|
|
2540
|
+
/**
|
|
2541
|
+
* Serve a `date` column as the timestamp this API says it serves.
|
|
2542
|
+
*
|
|
2543
|
+
* The OpenAPI document this server publishes types a `date` property as
|
|
2544
|
+
* `string, format: date-time` — RFC 3339 — and node-postgres hands over the
|
|
2545
|
+
* Postgres literal: `"2026-08-24 01:37:57.647+02"`, with a space where the `T`
|
|
2546
|
+
* belongs and a two-digit offset. V8 parses that by accident; Safari's
|
|
2547
|
+
* `new Date()` does not have to, and the spec never promised it. A column whose
|
|
2548
|
+
* documented type only parses in some browsers is not a contract.
|
|
2549
|
+
*
|
|
2550
|
+
* A date-only column (`mode: "date"`, `format: "date"` in the spec) is already
|
|
2551
|
+
* RFC 3339 as `YYYY-MM-DD` and is left alone — widening it to a timestamp would
|
|
2552
|
+
* invent a time of day and a timezone the column does not have.
|
|
2553
|
+
*/
|
|
2554
|
+
function toRestDate(value) {
|
|
2555
|
+
if (value instanceof Date) return isNaN(value.getTime()) ? null : value.toISOString();
|
|
2556
|
+
if (value && typeof value === "object" && value.__type === "date") return value.value ?? null;
|
|
2557
|
+
if (typeof value !== "string" && typeof value !== "number") return value;
|
|
2558
|
+
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value)) return value;
|
|
2559
|
+
const date = new Date(value);
|
|
2560
|
+
return isNaN(date.getTime()) ? value : date.toISOString();
|
|
2561
|
+
}
|
|
2562
|
+
/**
|
|
2563
|
+
* One scalar, as REST serves it: declared numbers as numbers, declared dates as
|
|
2564
|
+
* RFC 3339, everything else exactly as the database returned it.
|
|
2565
|
+
*/
|
|
2566
|
+
function toRestScalar(value, property) {
|
|
2567
|
+
if (property?.type === "date") return toRestDate(value);
|
|
2568
|
+
return coerceDeclaredNumber(value, property);
|
|
2569
|
+
}
|
|
2570
|
+
/**
|
|
2571
|
+
* Apply {@link toRestScalar} across a row, leaving undeclared columns alone.
|
|
2572
|
+
*
|
|
2573
|
+
* Exported because the include loader attaches related rows itself, and a
|
|
2574
|
+
* target rendered differently from its parent is the shape bug this whole file
|
|
2575
|
+
* exists to prevent — a date was a string at the top level and a
|
|
2576
|
+
* `{ __type: "date" }` envelope one level down, in the same response.
|
|
2577
|
+
*/
|
|
2578
|
+
function toRestValues(row, collection) {
|
|
2542
2579
|
const properties = collection.properties;
|
|
2543
2580
|
if (!properties) return row;
|
|
2544
2581
|
const out = {};
|
|
2545
|
-
for (const [key, value] of Object.entries(row)) out[key] =
|
|
2582
|
+
for (const [key, value] of Object.entries(row)) out[key] = toRestScalar(value, properties[key]);
|
|
2546
2583
|
return out;
|
|
2547
2584
|
}
|
|
2548
2585
|
/** Render one target row in the requested style. */
|
|
@@ -2588,7 +2625,7 @@ function stripUnreadable(row, collection) {
|
|
|
2588
2625
|
return row;
|
|
2589
2626
|
}
|
|
2590
2627
|
function renderTarget(targetRow, targetCollection, style, registry) {
|
|
2591
|
-
if (style === "inline") return stripUnreadable(
|
|
2628
|
+
if (style === "inline") return stripUnreadable(toRestValues({ ...targetRow }, targetCollection), targetCollection);
|
|
2592
2629
|
const address = relationTargetAddress(targetRow, targetCollection, registry);
|
|
2593
2630
|
const path = targetCollection.slug;
|
|
2594
2631
|
return createRelationRefWithData(address, path, {
|
|
@@ -2638,9 +2675,9 @@ function toFlatRow(row, collection, registry) {
|
|
|
2638
2675
|
*
|
|
2639
2676
|
* Values are the ones the database returned, except where that contradicts the
|
|
2640
2677
|
* declared type: a `number` property is served as a number (see
|
|
2641
|
-
* {@link coerceDeclaredNumber})
|
|
2642
|
-
*
|
|
2643
|
-
*
|
|
2678
|
+
* {@link coerceDeclaredNumber}) and a `date` as RFC 3339, which is what this
|
|
2679
|
+
* server's own OpenAPI document says a date column is (see
|
|
2680
|
+
* {@link toRestDate}).
|
|
2644
2681
|
*
|
|
2645
2682
|
* Keyed by the row rather than by the relation list — a REST fetch only loads
|
|
2646
2683
|
* the relations `include` asked for, so the row is the authority on which are
|
|
@@ -2653,7 +2690,7 @@ function toRestRow(row, collection, registry) {
|
|
|
2653
2690
|
const relation = findRelation(resolvedRelations, key);
|
|
2654
2691
|
if (relation && Array.isArray(value)) flat[key] = value.map((item) => renderTarget(isJunctionRelation(relation) ? unwrapJunctionRow(item) : item, relation.target(), "inline", registry));
|
|
2655
2692
|
else if (relation && typeof value === "object" && value !== null) flat[key] = renderTarget(value, relation.target(), "inline", registry);
|
|
2656
|
-
else flat[key] =
|
|
2693
|
+
else flat[key] = toRestScalar(value, collection.properties?.[key]);
|
|
2657
2694
|
}
|
|
2658
2695
|
return stripUnreadable(flat, collection);
|
|
2659
2696
|
}
|
|
@@ -3515,6 +3552,28 @@ function compareForSort(a, b, nullsLast) {
|
|
|
3515
3552
|
* Service for handling all row read operations.
|
|
3516
3553
|
* Handles fetching, searching, counting, and filtering rows.
|
|
3517
3554
|
*/
|
|
3555
|
+
/**
|
|
3556
|
+
* Which aggregate aliases hold a number that Postgres handed back as a string.
|
|
3557
|
+
*
|
|
3558
|
+
* `count`, `sum` and `avg` always do: bigint and numeric are returned as text
|
|
3559
|
+
* because they do not fit a JS number in general. `min` and `max` are uncast,
|
|
3560
|
+
* so they do it too — but only over a numeric column, and those are the two
|
|
3561
|
+
* functions that also apply to text and dates.
|
|
3562
|
+
*
|
|
3563
|
+
* Decided by the column's DECLARED type, not by trying `Number()` on the value.
|
|
3564
|
+
* `?select=avg(price),max(price)` used to answer `{avg_price: 5,
|
|
3565
|
+
* max_price: "8.5"}` — one column, two functions, two JSON types, and
|
|
3566
|
+
* arithmetic on the second one silently concatenates. Parsing whatever looks
|
|
3567
|
+
* numeric would fix that and break something worse: a `min(sku)` of `"00123"`
|
|
3568
|
+
* would come back as `123`, a different value, and no NaN check would catch it.
|
|
3569
|
+
*
|
|
3570
|
+
* Exported to be tested: the parsing itself needs a database, this decision
|
|
3571
|
+
* does not.
|
|
3572
|
+
*/
|
|
3573
|
+
function numericAggregateAliases(aggregates, properties) {
|
|
3574
|
+
const isNumberField = (field) => Boolean(field) && properties[field]?.type === "number";
|
|
3575
|
+
return new Set(aggregates.filter((a) => a.fn === "count" || a.fn === "sum" || a.fn === "avg" || (a.fn === "min" || a.fn === "max") && isNumberField(a.field)).map((a) => a.alias));
|
|
3576
|
+
}
|
|
3518
3577
|
var FetchService = class FetchService {
|
|
3519
3578
|
db;
|
|
3520
3579
|
registry;
|
|
@@ -3909,7 +3968,7 @@ var FetchService = class FetchService {
|
|
|
3909
3968
|
row[key] = null;
|
|
3910
3969
|
continue;
|
|
3911
3970
|
}
|
|
3912
|
-
const [shaped] = this.shapeRelatedRows([{ ...related.values }], node, targetCollection);
|
|
3971
|
+
const [shaped] = this.shapeRelatedRows([toRestValues({ ...related.values }, targetCollection)], node, targetCollection);
|
|
3913
3972
|
row[key] = shaped;
|
|
3914
3973
|
loaded.push(shaped);
|
|
3915
3974
|
}
|
|
@@ -3917,7 +3976,7 @@ var FetchService = class FetchService {
|
|
|
3917
3976
|
const results = await this.relationService.batchFetchRelatedEntitiesMany(collectionPath, rowIds, key, relation, narrow);
|
|
3918
3977
|
for (const row of addressable) {
|
|
3919
3978
|
const related = results.get(String(addressOf(row))) ?? [];
|
|
3920
|
-
const shaped = this.shapeRelatedRows(related.map((e) => ({ ...e.values })), node, targetCollection);
|
|
3979
|
+
const shaped = this.shapeRelatedRows(related.map((e) => toRestValues({ ...e.values }, targetCollection)), node, targetCollection);
|
|
3921
3980
|
row[key] = shaped;
|
|
3922
3981
|
loaded.push(...shaped);
|
|
3923
3982
|
}
|
|
@@ -4355,7 +4414,7 @@ var FetchService = class FetchService {
|
|
|
4355
4414
|
if (options.limit) query = query.limit(options.limit);
|
|
4356
4415
|
}
|
|
4357
4416
|
const rows = await query;
|
|
4358
|
-
const numericAliases =
|
|
4417
|
+
const numericAliases = numericAggregateAliases(options.aggregates, collection?.properties ?? {});
|
|
4359
4418
|
return rows.map((row) => {
|
|
4360
4419
|
const out = { ...row };
|
|
4361
4420
|
for (const alias of numericAliases) {
|
|
@@ -5120,7 +5179,7 @@ var PersistService = class {
|
|
|
5120
5179
|
} catch (error) {
|
|
5121
5180
|
throw this.toUserFriendlyError(error, collection.slug, collection);
|
|
5122
5181
|
}
|
|
5123
|
-
const finalEntity = await this.fetchService.fetchOneForRest(collection.slug, savedId, void 0, databaseId);
|
|
5182
|
+
const finalEntity = await this.fetchService.fetchOneForRest(collection.slug, savedId, void 0, databaseId, { withDeleted: true });
|
|
5124
5183
|
if (!finalEntity) throw new Error("Could not fetch row after save.");
|
|
5125
5184
|
return finalEntity;
|
|
5126
5185
|
}
|