@rebasepro/server-postgres 0.13.0 → 0.13.1-canary.g3660bd5
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/{ensure-collection-policies-ViG8XiPn.js → ensure-collection-policies-25nZOd87.js} +2 -2
- package/dist/{ensure-collection-policies-ViG8XiPn.js.map → ensure-collection-policies-25nZOd87.js.map} +1 -1
- package/dist/{ensure-collection-tables-CBQdOETu.js → ensure-collection-tables-DRkgVBV5.js} +2 -2
- package/dist/{ensure-collection-tables-CBQdOETu.js.map → ensure-collection-tables-DRkgVBV5.js.map} +1 -1
- package/dist/index.es.js +110 -93
- package/dist/index.es.js.map +1 -1
- package/dist/schema/generated-schema-staleness.d.ts +39 -0
- package/dist/services/FetchService.d.ts +10 -7
- package/dist/services/dataService.d.ts +2 -0
- package/dist/services/realtimeService.d.ts +25 -21
- package/dist/{src-DlPBctw_.js → src-Bs2ZzSi4.js} +92 -15
- package/dist/src-Bs2ZzSi4.js.map +1 -0
- package/package.json +6 -6
- package/src/cli.ts +79 -0
- package/src/collections/validate-relations.ts +124 -17
- package/src/data-transformer.ts +13 -3
- package/src/schema/generated-schema-staleness.ts +164 -0
- package/src/schema/introspect-db-logic.ts +1 -1
- package/src/services/FetchService.ts +10 -93
- package/src/services/PersistService.ts +14 -1
- package/src/services/dataService.ts +2 -0
- package/src/services/realtimeService.ts +36 -33
- package/dist/src-DlPBctw_.js.map +0 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { CollectionConfig } from "@rebasepro/types";
|
|
2
|
+
/**
|
|
3
|
+
* Notice a generated Drizzle schema that a *library upgrade* invalidated.
|
|
4
|
+
*
|
|
5
|
+
* `rebase dev` already watches `config/collections` and warns when a collection
|
|
6
|
+
* file changes. That covers drift the developer caused. It cannot cover this one,
|
|
7
|
+
* because nothing the developer owns changed: 0.13 derives `category_id` where
|
|
8
|
+
* 0.12 derived `categorie_id`, from the same unedited collection. The watcher
|
|
9
|
+
* never fires, and `backend/src/schema.generated.ts` quietly stops describing the
|
|
10
|
+
* schema the runtime expects.
|
|
11
|
+
*
|
|
12
|
+
* The consequence is not cosmetic. Boot-ensure renames the column in the
|
|
13
|
+
* database, then relation validation reads the stale module and refuses to
|
|
14
|
+
* start — on that boot and every boot after it, because the rename is already
|
|
15
|
+
* applied and will not be attempted again.
|
|
16
|
+
*
|
|
17
|
+
* Deliberately narrow: this answers "does the generated schema name a foreign key
|
|
18
|
+
* the way the previous rule did", not "is this file what we would generate now".
|
|
19
|
+
* The wide question would report every whitespace change in the generator as a
|
|
20
|
+
* fatal staleness, and a check that cries wolf gets switched off.
|
|
21
|
+
*/
|
|
22
|
+
/** One column the generated schema names under the pre-0.13 rule. */
|
|
23
|
+
export interface LegacyForeignKeyName {
|
|
24
|
+
/** Table whose column declaration is stale. */
|
|
25
|
+
table: string;
|
|
26
|
+
/** The name the generated schema declares. */
|
|
27
|
+
legacy: string;
|
|
28
|
+
/** The name this release derives, and which the database now carries. */
|
|
29
|
+
current: string;
|
|
30
|
+
/** `<collection>.<relation>` that derives it, for the message. */
|
|
31
|
+
relation: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* @param generatedSource contents of `backend/src/schema.generated.ts`
|
|
35
|
+
* @param collections the project's collections, as this release reads them
|
|
36
|
+
*/
|
|
37
|
+
export declare function findLegacyForeignKeyNames(generatedSource: string, collections: CollectionConfig[]): LegacyForeignKeyName[];
|
|
38
|
+
/** One-line summary for a log or a CLI notice. */
|
|
39
|
+
export declare function describeLegacyForeignKeyNames(found: LegacyForeignKeyName[]): string;
|
|
@@ -153,6 +153,16 @@ export declare class FetchService {
|
|
|
153
153
|
*/
|
|
154
154
|
fetchCollection<M extends Record<string, unknown>>(collectionPath: string, options?: {
|
|
155
155
|
filter?: FilterValues<Extract<keyof M, string>>;
|
|
156
|
+
/**
|
|
157
|
+
* An `or(...)`/`and(...)` group, applied alongside `filter`.
|
|
158
|
+
*
|
|
159
|
+
* `fetchRowsWithConditions` below has always applied this; it was
|
|
160
|
+
* simply absent from this signature, so the only callers that could
|
|
161
|
+
* pass one were the ones that went around this method. Realtime
|
|
162
|
+
* came through here, which is why a subscription filtered by a
|
|
163
|
+
* logical group was pushed every row in the table.
|
|
164
|
+
*/
|
|
165
|
+
logical?: LogicalCondition;
|
|
156
166
|
orderBy?: string;
|
|
157
167
|
order?: "desc" | "asc";
|
|
158
168
|
limit?: number;
|
|
@@ -226,13 +236,6 @@ export declare class FetchService {
|
|
|
226
236
|
* Note: Primary path now uses inline `getQueryBuilder()` checks.
|
|
227
237
|
*/
|
|
228
238
|
private hasDrizzleQueryAPI;
|
|
229
|
-
/**
|
|
230
|
-
* Attempt to use Drizzle's relational query API (db.query.<table>.findMany)
|
|
231
|
-
* for efficient JOIN-based relation loading.
|
|
232
|
-
* Returns null if the API is not available or the query fails.
|
|
233
|
-
* Note: Primary path now uses `buildWithConfig` + `buildDrizzleQueryOptions`.
|
|
234
|
-
*/
|
|
235
|
-
private fetchWithDrizzleQuery;
|
|
236
239
|
/**
|
|
237
240
|
* Fallback path used when db.query is unavailable.
|
|
238
241
|
* The primary path uses db.query.findMany with `with` config, which
|
|
@@ -36,6 +36,8 @@ export declare class DataService implements DataRepository {
|
|
|
36
36
|
*/
|
|
37
37
|
fetchCollection<M extends Record<string, unknown>>(collectionPath: string, options?: {
|
|
38
38
|
filter?: FilterValues<Extract<keyof M, string>>;
|
|
39
|
+
/** An `or(...)`/`and(...)` group, applied alongside `filter`. */
|
|
40
|
+
logical?: LogicalCondition;
|
|
39
41
|
orderBy?: string;
|
|
40
42
|
order?: "desc" | "asc";
|
|
41
43
|
limit?: number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { WebSocket } from "ws";
|
|
2
2
|
import { EventEmitter } from "events";
|
|
3
|
-
import { DataDriver, WebSocketMessage } from "@rebasepro/types";
|
|
3
|
+
import { DataDriver, WebSocketMessage, LogicalCondition } from "@rebasepro/types";
|
|
4
4
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
5
5
|
import { RealtimeProvider, CollectionSubscriptionConfig, SingleSubscriptionConfig } from "../interfaces";
|
|
6
6
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
@@ -14,6 +14,27 @@ export interface SubscriptionAuthContext {
|
|
|
14
14
|
uid: string;
|
|
15
15
|
roles: string[];
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* The narrowing a collection subscription was created with, kept so that every
|
|
19
|
+
* refetch answers the same query the initial fetch did.
|
|
20
|
+
*
|
|
21
|
+
* Named once because it used to be written out inline in five places, and a
|
|
22
|
+
* field missing from one of them is accepted over the wire and then silently
|
|
23
|
+
* ignored: `offset` was declared on the incoming props and never stored, so a
|
|
24
|
+
* live list on page three served page one, and `logical` was never stored
|
|
25
|
+
* either, so an `or(...)` subscription was pushed every row in the table.
|
|
26
|
+
*/
|
|
27
|
+
type StoredCollectionRequest = {
|
|
28
|
+
filter?: Record<string, unknown>;
|
|
29
|
+
logical?: LogicalCondition;
|
|
30
|
+
orderBy?: string;
|
|
31
|
+
order?: "desc" | "asc";
|
|
32
|
+
limit?: number;
|
|
33
|
+
offset?: number;
|
|
34
|
+
startAfter?: Record<string, unknown>;
|
|
35
|
+
databaseId?: string;
|
|
36
|
+
searchString?: string;
|
|
37
|
+
};
|
|
17
38
|
/**
|
|
18
39
|
* PostgreSQL-specific realtime service.
|
|
19
40
|
* Handles WebSocket connections and subscriptions for real-time row updates.
|
|
@@ -128,16 +149,7 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
128
149
|
type: "collection" | "single";
|
|
129
150
|
path: string;
|
|
130
151
|
id?: string | number;
|
|
131
|
-
collectionRequest?:
|
|
132
|
-
filter?: Record<string, unknown>;
|
|
133
|
-
orderBy?: string;
|
|
134
|
-
order?: "desc" | "asc";
|
|
135
|
-
limit?: number;
|
|
136
|
-
offset?: number;
|
|
137
|
-
startAfter?: Record<string, unknown>;
|
|
138
|
-
databaseId?: string;
|
|
139
|
-
searchString?: string;
|
|
140
|
-
};
|
|
152
|
+
collectionRequest?: StoredCollectionRequest;
|
|
141
153
|
authContext?: SubscriptionAuthContext;
|
|
142
154
|
}>;
|
|
143
155
|
registerDataDriverSubscription(subscriptionId: string, subscription: {
|
|
@@ -145,16 +157,7 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
145
157
|
type: "collection" | "single";
|
|
146
158
|
path: string;
|
|
147
159
|
id?: string | number;
|
|
148
|
-
collectionRequest?:
|
|
149
|
-
filter?: Record<string, unknown>;
|
|
150
|
-
orderBy?: string;
|
|
151
|
-
order?: "desc" | "asc";
|
|
152
|
-
limit?: number;
|
|
153
|
-
offset?: number;
|
|
154
|
-
startAfter?: Record<string, unknown>;
|
|
155
|
-
databaseId?: string;
|
|
156
|
-
searchString?: string;
|
|
157
|
-
};
|
|
160
|
+
collectionRequest?: StoredCollectionRequest;
|
|
158
161
|
authContext?: SubscriptionAuthContext;
|
|
159
162
|
}): void;
|
|
160
163
|
addSubscriptionCallback(subscriptionId: string, callback: (data: Record<string, unknown>[] | Record<string, unknown> | null) => void): void;
|
|
@@ -497,3 +500,4 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
497
500
|
* This allows code to use PostgresRealtimeProvider alongside future MongoRealtimeProvider, etc.
|
|
498
501
|
*/
|
|
499
502
|
export declare const PostgresRealtimeProvider: typeof RealtimeService;
|
|
503
|
+
export {};
|
|
@@ -55,6 +55,25 @@ function hasForeignKeyOnTarget(relation) {
|
|
|
55
55
|
function isManyToMany(relation) {
|
|
56
56
|
return relation.kind === "manyToMany";
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Resolve a client-supplied list `limit` into a safe, always-defined value.
|
|
60
|
+
*
|
|
61
|
+
* - A provided limit is coerced to an integer and clamped to `[1, maxLimit]`,
|
|
62
|
+
* so `0`, negatives, and absurd values can never bypass the cap.
|
|
63
|
+
* - An absent / blank / non-numeric limit falls back to the mode default:
|
|
64
|
+
* `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`.
|
|
65
|
+
*
|
|
66
|
+
* The return is never `undefined` — no ingress that routes its client limit
|
|
67
|
+
* through this can produce an unbounded read.
|
|
68
|
+
*/
|
|
69
|
+
function resolveClientListLimit(rawLimit, opts = {}) {
|
|
70
|
+
const maxLimit = opts.maxLimit ?? 1e3;
|
|
71
|
+
if (rawLimit != null && String(rawLimit).trim() !== "") {
|
|
72
|
+
const parsed = typeof rawLimit === "number" ? rawLimit : parseInt(String(rawLimit), 10);
|
|
73
|
+
if (Number.isFinite(parsed)) return Math.min(Math.max(1, Math.floor(parsed)), maxLimit);
|
|
74
|
+
}
|
|
75
|
+
return opts.vectorSearch ? opts.vectorDefaultLimit ?? 10 : opts.defaultLimit ?? 50;
|
|
76
|
+
}
|
|
58
77
|
//#endregion
|
|
59
78
|
//#region ../common/src/util/common.ts
|
|
60
79
|
var DEFAULT_ONE_OF_TYPE = "type";
|
|
@@ -1007,6 +1026,35 @@ function camelCase(str) {
|
|
|
1007
1026
|
});
|
|
1008
1027
|
})))();
|
|
1009
1028
|
/**
|
|
1029
|
+
* Segments that reach the prototype chain rather than a property of the object.
|
|
1030
|
+
*
|
|
1031
|
+
* The twin of this function in `@rebasepro/forms` could be made to write onto
|
|
1032
|
+
* `Object.prototype` through a path of `__proto__.x`. This copy survives the
|
|
1033
|
+
* write by accident — its `clone` always spreads into a fresh object, while the
|
|
1034
|
+
* form engine's has a "preserve class instances" branch that hands back
|
|
1035
|
+
* `Object.prototype` itself — but `getIn` still *reads* through the chain, and
|
|
1036
|
+
* handing back `Object.prototype` is how a polluted value is read out again.
|
|
1037
|
+
*
|
|
1038
|
+
* Closed on both sides here, so the two implementations agree.
|
|
1039
|
+
*/
|
|
1040
|
+
var UNSAFE_PATH_SEGMENTS = /* @__PURE__ */ new Set([
|
|
1041
|
+
"__proto__",
|
|
1042
|
+
"constructor",
|
|
1043
|
+
"prototype"
|
|
1044
|
+
]);
|
|
1045
|
+
/**
|
|
1046
|
+
* Whether writing this single key with `obj[key] = …` would reach the prototype
|
|
1047
|
+
* chain instead of creating a property.
|
|
1048
|
+
*
|
|
1049
|
+
* The single-key counterpart of {@link pathTraversesPrototype}, for the many
|
|
1050
|
+
* places that copy an object one key at a time. `JSON.parse` creates
|
|
1051
|
+
* `__proto__` as an *own* property, so it survives `hasOwnProperty` — and then
|
|
1052
|
+
* `target[key] = value` invokes the setter and replaces the target's prototype.
|
|
1053
|
+
*/
|
|
1054
|
+
function isPrototypePollutingKey(key) {
|
|
1055
|
+
return UNSAFE_PATH_SEGMENTS.has(key);
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1010
1058
|
* Deep clone a value, preserving function references and class instances.
|
|
1011
1059
|
* Unlike structuredClone, this handles objects that contain functions
|
|
1012
1060
|
* (e.g. CollectionConfig with target(), childCollections(), callbacks).
|
|
@@ -3660,6 +3708,30 @@ var RebasePaginationError = class RebasePaginationError extends Error {
|
|
|
3660
3708
|
Object.setPrototypeOf(this, RebasePaginationError.prototype);
|
|
3661
3709
|
}
|
|
3662
3710
|
};
|
|
3711
|
+
/**
|
|
3712
|
+
* Resolve `limit`/`offset`/`page` into the window a read will actually use.
|
|
3713
|
+
*
|
|
3714
|
+
* Lives here, next to the walk, for the reason at the top of this file: every
|
|
3715
|
+
* transport has to mean the same thing by "page two". Four of them did not —
|
|
3716
|
+
* the REST layer strode by {@link DEFAULT_LIST_LIMIT}, the local-first
|
|
3717
|
+
* evaluator by {@link DEFAULT_PAGE_SIZE}, the in-process accessor by 20, and
|
|
3718
|
+
* the published type documented a fourth number. Pages that overlap or skip
|
|
3719
|
+
* rows are the mildest of those outcomes.
|
|
3720
|
+
*
|
|
3721
|
+
* `page` wins over `offset`, as {@link FindParams} documents. `driverOffset`
|
|
3722
|
+
* is the value to hand a driver: it stays `undefined` when the caller named no
|
|
3723
|
+
* offset, because keyset pagination seeks with a `where` clause and must not
|
|
3724
|
+
* look like it is paging by offset.
|
|
3725
|
+
*/
|
|
3726
|
+
function resolveFindWindow(params) {
|
|
3727
|
+
const limit = params?.limit ?? 50;
|
|
3728
|
+
const offset = params?.page != null ? Math.max(0, (params.page - 1) * limit) : params?.offset ?? 0;
|
|
3729
|
+
return {
|
|
3730
|
+
limit,
|
|
3731
|
+
offset,
|
|
3732
|
+
driverOffset: params?.page != null ? offset : params?.offset
|
|
3733
|
+
};
|
|
3734
|
+
}
|
|
3663
3735
|
function normalizePageSize(raw) {
|
|
3664
3736
|
if (raw === void 0 || !Number.isFinite(raw)) return 200;
|
|
3665
3737
|
return Math.max(1, Math.floor(raw));
|
|
@@ -3955,21 +4027,22 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
3955
4027
|
const accessor = {
|
|
3956
4028
|
async find(params) {
|
|
3957
4029
|
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
3958
|
-
const limit = params
|
|
3959
|
-
const offset = params?.offset ?? 0;
|
|
4030
|
+
const { limit, offset, driverOffset } = resolveFindWindow(params);
|
|
3960
4031
|
const fetchService = driver.restFetchService;
|
|
3961
4032
|
const rows = fetchService ? await fetchService.fetchCollectionForRest(slug, {
|
|
3962
4033
|
filter,
|
|
3963
|
-
|
|
3964
|
-
|
|
4034
|
+
logical: params?.logical,
|
|
4035
|
+
limit,
|
|
4036
|
+
offset: driverOffset,
|
|
3965
4037
|
orderBy: params?.orderBy?.[0],
|
|
3966
4038
|
order: params?.orderBy?.[1],
|
|
3967
4039
|
searchString: params?.searchString
|
|
3968
4040
|
}, params?.include) : await driver.fetchCollection({
|
|
3969
4041
|
path: slug,
|
|
3970
|
-
limit
|
|
3971
|
-
offset:
|
|
4042
|
+
limit,
|
|
4043
|
+
offset: driverOffset,
|
|
3972
4044
|
filter,
|
|
4045
|
+
logical: params?.logical,
|
|
3973
4046
|
orderBy: params?.orderBy?.[0],
|
|
3974
4047
|
order: params?.orderBy?.[1],
|
|
3975
4048
|
searchString: params?.searchString
|
|
@@ -3979,7 +4052,9 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
3979
4052
|
if (driver.count) {
|
|
3980
4053
|
total = await driver.count({
|
|
3981
4054
|
path: slug,
|
|
3982
|
-
filter
|
|
4055
|
+
filter,
|
|
4056
|
+
logical: params?.logical,
|
|
4057
|
+
searchString: params?.searchString
|
|
3983
4058
|
});
|
|
3984
4059
|
hasMore = offset + rows.length < total;
|
|
3985
4060
|
}
|
|
@@ -4035,18 +4110,20 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
4035
4110
|
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
4036
4111
|
return driver.count({
|
|
4037
4112
|
path: slug,
|
|
4038
|
-
filter
|
|
4113
|
+
filter,
|
|
4114
|
+
logical: params?.logical,
|
|
4115
|
+
searchString: params?.searchString
|
|
4039
4116
|
});
|
|
4040
4117
|
} : void 0,
|
|
4041
4118
|
listen: driver.listenCollection ? (params, onUpdate, onError) => {
|
|
4042
|
-
const limit = params
|
|
4043
|
-
const offset = params?.offset ?? 0;
|
|
4119
|
+
const { limit, offset, driverOffset } = resolveFindWindow(params);
|
|
4044
4120
|
const normalize = driver.restFetchService ? inlineRelationRefs : (row) => row;
|
|
4045
4121
|
return driver.listenCollection({
|
|
4046
4122
|
path: slug,
|
|
4047
|
-
limit
|
|
4048
|
-
offset:
|
|
4123
|
+
limit,
|
|
4124
|
+
offset: driverOffset,
|
|
4049
4125
|
filter: params?.where,
|
|
4126
|
+
logical: params?.logical,
|
|
4050
4127
|
orderBy: params?.orderBy?.[0],
|
|
4051
4128
|
order: params?.orderBy?.[1],
|
|
4052
4129
|
searchString: params?.searchString,
|
|
@@ -4054,7 +4131,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
4054
4131
|
onUpdate({
|
|
4055
4132
|
data: entities.map((row) => rowToEntity(normalize(row), slug, getPks())),
|
|
4056
4133
|
meta: {
|
|
4057
|
-
total: entities.length,
|
|
4134
|
+
total: offset + entities.length,
|
|
4058
4135
|
limit,
|
|
4059
4136
|
offset,
|
|
4060
4137
|
hasMore: entities.length >= limit
|
|
@@ -4294,6 +4371,6 @@ function buildSdkData(driver) {
|
|
|
4294
4371
|
return wrapAsSdkData(buildRebaseData(driver));
|
|
4295
4372
|
}
|
|
4296
4373
|
//#endregion
|
|
4297
|
-
export {
|
|
4374
|
+
export { camelCase as A, normalizeToEntityRelation as C, getPolicyNamesForRule as D, legacyForeignKeyName as E, hasForeignKeyOnTarget as F, isManyToMany as I, Vector as L, DEFAULT_ONE_OF_TYPE as M, DEFAULT_ONE_OF_VALUE as N, isPrototypePollutingKey as O, resolveClientListLimit as P, createRelationRefWithData as S, generateForeignKeyName as T, buildCompositeId as _, getJunctionSecurityRules as a, parseIdValues as b, policyToPostgres as c, findRelation as d, getColumnName as f, resolveCollectionRelations as g, getTableVarName as h, getJunctionCollectionConfig as i, toSnakeCase as j, mergeDeep as k, securityRuleToConditions as l, getTableName as m, CollectionRegistry as n, resolveJunctionSpecs as o, getEnumVarName as p, resolveStringColumnLength as r, getEffectiveSecurityRules as s, buildSdkData as t, findAnonymousGrants as u, getDeclaredPrimaryKeys as v, updateDateAutoValues as w, createRelationRef as x, isAddressableId as y };
|
|
4298
4375
|
|
|
4299
|
-
//# sourceMappingURL=src-
|
|
4376
|
+
//# sourceMappingURL=src-Bs2ZzSi4.js.map
|