@rebasepro/server-postgresql 0.0.1-canary.4d4fb3e → 0.0.1-canary.ca2cb6e
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/common/src/collections/CollectionRegistry.d.ts +8 -0
- package/dist/common/src/util/entities.d.ts +22 -0
- package/dist/common/src/util/relations.d.ts +14 -4
- package/dist/common/src/util/resolutions.d.ts +1 -1
- package/dist/index.es.js +1254 -591
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1254 -591
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +17 -29
- package/dist/server-postgresql/src/auth/services.d.ts +7 -3
- package/dist/server-postgresql/src/collections/PostgresCollectionRegistry.d.ts +1 -1
- package/dist/server-postgresql/src/connection.d.ts +34 -1
- package/dist/server-postgresql/src/data-transformer.d.ts +26 -4
- package/dist/server-postgresql/src/databasePoolManager.d.ts +2 -2
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +139 -38
- package/dist/server-postgresql/src/schema/doctor-cli.d.ts +2 -0
- package/dist/server-postgresql/src/schema/doctor.d.ts +43 -0
- package/dist/server-postgresql/src/schema/generate-drizzle-schema-logic.d.ts +1 -1
- package/dist/server-postgresql/src/schema/test-schema.d.ts +24 -0
- package/dist/server-postgresql/src/services/EntityFetchService.d.ts +22 -8
- package/dist/server-postgresql/src/services/EntityPersistService.d.ts +1 -1
- package/dist/server-postgresql/src/services/RelationService.d.ts +11 -5
- package/dist/server-postgresql/src/services/entity-helpers.d.ts +16 -2
- package/dist/server-postgresql/src/services/entityService.d.ts +8 -6
- package/dist/server-postgresql/src/services/realtimeService.d.ts +2 -0
- package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +2 -2
- package/dist/types/src/controllers/auth.d.ts +2 -0
- package/dist/types/src/controllers/client.d.ts +119 -7
- package/dist/types/src/controllers/collection_registry.d.ts +4 -3
- package/dist/types/src/controllers/customization_controller.d.ts +7 -1
- package/dist/types/src/controllers/data.d.ts +34 -7
- package/dist/types/src/controllers/data_driver.d.ts +20 -28
- package/dist/types/src/controllers/database_admin.d.ts +2 -2
- package/dist/types/src/controllers/email.d.ts +34 -0
- package/dist/types/src/controllers/index.d.ts +1 -0
- package/dist/types/src/controllers/local_config_persistence.d.ts +4 -4
- package/dist/types/src/controllers/navigation.d.ts +5 -5
- package/dist/types/src/controllers/registry.d.ts +6 -3
- package/dist/types/src/controllers/side_entity_controller.d.ts +7 -6
- package/dist/types/src/controllers/storage.d.ts +24 -26
- package/dist/types/src/rebase_context.d.ts +8 -4
- package/dist/types/src/types/backend.d.ts +4 -1
- package/dist/types/src/types/builders.d.ts +5 -4
- package/dist/types/src/types/chips.d.ts +1 -1
- package/dist/types/src/types/collections.d.ts +169 -125
- package/dist/types/src/types/cron.d.ts +102 -0
- package/dist/types/src/types/data_source.d.ts +1 -1
- package/dist/types/src/types/entity_actions.d.ts +8 -8
- package/dist/types/src/types/entity_callbacks.d.ts +15 -15
- package/dist/types/src/types/entity_link_builder.d.ts +1 -1
- package/dist/types/src/types/entity_overrides.d.ts +2 -1
- package/dist/types/src/types/entity_views.d.ts +8 -8
- package/dist/types/src/types/export_import.d.ts +3 -3
- package/dist/types/src/types/index.d.ts +1 -0
- package/dist/types/src/types/plugins.d.ts +72 -18
- package/dist/types/src/types/properties.d.ts +118 -33
- package/dist/types/src/types/relations.d.ts +1 -1
- package/dist/types/src/types/slots.d.ts +30 -6
- package/dist/types/src/types/translations.d.ts +44 -0
- package/dist/types/src/types/user_management_delegate.d.ts +1 -0
- package/drizzle-test/0000_woozy_junta.sql +6 -0
- package/drizzle-test/0001_youthful_arachne.sql +1 -0
- package/drizzle-test/0002_lively_dragon_lord.sql +2 -0
- package/drizzle-test/0003_mean_king_cobra.sql +2 -0
- package/drizzle-test/meta/0000_snapshot.json +47 -0
- package/drizzle-test/meta/0001_snapshot.json +48 -0
- package/drizzle-test/meta/0002_snapshot.json +38 -0
- package/drizzle-test/meta/0003_snapshot.json +48 -0
- package/drizzle-test/meta/_journal.json +34 -0
- package/drizzle-test-out/0000_tan_trauma.sql +6 -0
- package/drizzle-test-out/0001_rapid_drax.sql +1 -0
- package/drizzle-test-out/meta/0000_snapshot.json +44 -0
- package/drizzle-test-out/meta/0001_snapshot.json +54 -0
- package/drizzle-test-out/meta/_journal.json +20 -0
- package/drizzle.test.config.ts +10 -0
- package/package.json +88 -89
- package/scratch.ts +41 -0
- package/src/PostgresBackendDriver.ts +63 -79
- package/src/PostgresBootstrapper.ts +7 -8
- package/src/auth/ensure-tables.ts +158 -86
- package/src/auth/services.ts +109 -50
- package/src/cli.ts +259 -16
- package/src/collections/PostgresCollectionRegistry.ts +6 -6
- package/src/connection.ts +70 -48
- package/src/data-transformer.ts +155 -116
- package/src/databasePoolManager.ts +6 -5
- package/src/history/HistoryService.ts +3 -12
- package/src/interfaces.ts +3 -3
- package/src/schema/auth-schema.ts +26 -3
- package/src/schema/doctor-cli.ts +47 -0
- package/src/schema/doctor.ts +595 -0
- package/src/schema/generate-drizzle-schema-logic.ts +204 -57
- package/src/schema/generate-drizzle-schema.ts +6 -6
- package/src/schema/test-schema.ts +11 -0
- package/src/services/BranchService.ts +5 -5
- package/src/services/EntityFetchService.ts +317 -188
- package/src/services/EntityPersistService.ts +15 -17
- package/src/services/RelationService.ts +299 -37
- package/src/services/entity-helpers.ts +39 -13
- package/src/services/entityService.ts +11 -9
- package/src/services/realtimeService.ts +58 -29
- package/src/utils/drizzle-conditions.ts +25 -24
- package/src/websocket.ts +52 -21
- package/test/auth-services.test.ts +131 -39
- package/test/batch-many-to-many-regression.test.ts +573 -0
- package/test/branchService.test.ts +22 -12
- package/test/data-transformer-hardening.test.ts +417 -0
- package/test/data-transformer.test.ts +175 -0
- package/test/doctor.test.ts +182 -0
- package/test/entityService.errors.test.ts +31 -16
- package/test/entityService.relations.test.ts +155 -59
- package/test/entityService.subcollection-search.test.ts +107 -57
- package/test/entityService.test.ts +105 -47
- package/test/generate-drizzle-schema.test.ts +262 -69
- package/test/historyService.test.ts +31 -16
- package/test/n-plus-one-regression.test.ts +314 -0
- package/test/postgresDataDriver.test.ts +260 -168
- package/test/realtimeService.test.ts +70 -39
- package/test/relation-pipeline-gaps.test.ts +637 -0
- package/test/relations.test.ts +492 -39
- package/test-drizzle-bug.ts +18 -0
- package/test-drizzle-out/0000_cultured_freak.sql +7 -0
- package/test-drizzle-out/0001_tiresome_professor_monster.sql +1 -0
- package/test-drizzle-out/meta/0000_snapshot.json +55 -0
- package/test-drizzle-out/meta/0001_snapshot.json +63 -0
- package/test-drizzle-out/meta/_journal.json +20 -0
- package/test-drizzle-prompt.sh +2 -0
- package/test-policy-prompt.sh +3 -0
- package/test-programmatic.ts +30 -0
- package/test-programmatic2.ts +59 -0
- package/test-schema-no-policies.ts +12 -0
- package/test_drizzle_mock.js +2 -2
- package/test_find_changed.mjs +3 -1
- package/test_hash.js +14 -0
- package/tsconfig.json +1 -1
- package/vite.config.ts +5 -5
|
@@ -11,10 +11,31 @@ import { getTableName } from "@rebasepro/common";
|
|
|
11
11
|
* `PostgresCollectionRegistry` instance — there is no global singleton.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Interface for Drizzle column metadata introspection.
|
|
16
|
+
* Replaces unsafe `as unknown as Record<string, unknown>` double-cast chains.
|
|
17
|
+
*/
|
|
18
|
+
export interface DrizzleColumnMeta {
|
|
19
|
+
columnType?: string;
|
|
20
|
+
dataType?: string;
|
|
21
|
+
primary?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Safely extract Drizzle column metadata from a column object. */
|
|
25
|
+
export function getColumnMeta(col: AnyPgColumn): DrizzleColumnMeta {
|
|
26
|
+
const raw = col as unknown as Record<string | symbol, unknown>;
|
|
27
|
+
return {
|
|
28
|
+
columnType: typeof raw.columnType === "string" ? raw.columnType : undefined,
|
|
29
|
+
dataType: typeof raw.dataType === "string" ? raw.dataType : undefined,
|
|
30
|
+
primary: typeof raw.primary === "boolean" ? raw.primary : undefined
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
14
34
|
export function getCollectionByPath(collectionPath: string, registry: PostgresCollectionRegistry): EntityCollection {
|
|
15
35
|
const collection = registry.getCollectionByPath(collectionPath);
|
|
16
36
|
if (!collection) {
|
|
17
|
-
|
|
37
|
+
const registered = registry.getCollections().map(c => c.slug).join(", ");
|
|
38
|
+
throw new Error(`Collection not found: ${collectionPath}. Registered collections: [${registered}]`);
|
|
18
39
|
}
|
|
19
40
|
return collection;
|
|
20
41
|
}
|
|
@@ -28,16 +49,17 @@ export function getTableForCollection(collection: EntityCollection, registry: Po
|
|
|
28
49
|
return table;
|
|
29
50
|
}
|
|
30
51
|
|
|
31
|
-
export function getPrimaryKeys(collection: EntityCollection, registry: PostgresCollectionRegistry): { fieldName: string; type: "string" | "number" }[] {
|
|
52
|
+
export function getPrimaryKeys(collection: EntityCollection, registry: PostgresCollectionRegistry): { fieldName: string; type: "string" | "number"; isUUID?: boolean }[] {
|
|
32
53
|
const table = getTableForCollection(collection, registry);
|
|
33
54
|
|
|
34
55
|
// Fallback to explicitly defined isId properties
|
|
35
56
|
if (collection.properties) {
|
|
36
57
|
const idProps = Object.entries(collection.properties)
|
|
37
|
-
.filter(([_, prop]) => "isId" in (prop as object) && Boolean((prop as
|
|
58
|
+
.filter(([_, prop]) => "isId" in (prop as object) && Boolean((prop as { isId?: unknown }).isId))
|
|
38
59
|
.map(([key, prop]) => ({
|
|
39
60
|
fieldName: key,
|
|
40
|
-
type: prop.type === "number" ? "number" as const : "string" as const
|
|
61
|
+
type: prop.type === "number" ? "number" as const : "string" as const,
|
|
62
|
+
isUUID: (prop as { isId?: unknown }).isId === "uuid"
|
|
41
63
|
}));
|
|
42
64
|
|
|
43
65
|
if (idProps.length > 0) {
|
|
@@ -46,12 +68,14 @@ export function getPrimaryKeys(collection: EntityCollection, registry: PostgresC
|
|
|
46
68
|
}
|
|
47
69
|
|
|
48
70
|
// Otherwise infer from Drizzle schema
|
|
49
|
-
const keys: { fieldName: string; type: "string" | "number" }[] = [];
|
|
71
|
+
const keys: { fieldName: string; type: "string" | "number"; isUUID?: boolean }[] = [];
|
|
50
72
|
for (const [key, colRaw] of Object.entries(table)) {
|
|
51
73
|
const col = colRaw as AnyPgColumn;
|
|
52
74
|
if (col && typeof col === "object" && "primary" in col && col.primary) {
|
|
53
|
-
const
|
|
54
|
-
|
|
75
|
+
const meta = getColumnMeta(col);
|
|
76
|
+
const type = col.dataType === "number" || meta.columnType === "PgSerial" || meta.columnType === "PgInteger" ? "number" : "string";
|
|
77
|
+
const isUUID = meta.columnType === "PgUUID";
|
|
78
|
+
keys.push({ fieldName: key, type, isUUID });
|
|
55
79
|
}
|
|
56
80
|
}
|
|
57
81
|
|
|
@@ -59,14 +83,16 @@ export function getPrimaryKeys(collection: EntityCollection, registry: PostgresC
|
|
|
59
83
|
// This maintains backwards compatibility
|
|
60
84
|
if (keys.length === 0 && "id" in table) {
|
|
61
85
|
const idCol = table["id" as keyof typeof table] as AnyPgColumn;
|
|
62
|
-
const
|
|
63
|
-
|
|
86
|
+
const idMeta = getColumnMeta(idCol);
|
|
87
|
+
const type = idCol.dataType === "number" || idMeta.columnType === "PgSerial" || idMeta.columnType === "PgInteger" ? "number" : "string";
|
|
88
|
+
const isUUID = idMeta.columnType === "PgUUID";
|
|
89
|
+
keys.push({ fieldName: "id", type, isUUID });
|
|
64
90
|
}
|
|
65
91
|
|
|
66
92
|
return keys;
|
|
67
93
|
}
|
|
68
94
|
|
|
69
|
-
export function parseIdValues(idValue: string | number, primaryKeys: { fieldName: string; type: "string" | "number" }[]): Record<string, string | number> {
|
|
95
|
+
export function parseIdValues(idValue: string | number, primaryKeys: { fieldName: string; type: "string" | "number"; isUUID?: boolean }[]): Record<string, string | number> {
|
|
70
96
|
const result: Record<string, string | number> = {};
|
|
71
97
|
|
|
72
98
|
if (primaryKeys.length === 0) {
|
|
@@ -75,7 +101,7 @@ export function parseIdValues(idValue: string | number, primaryKeys: { fieldName
|
|
|
75
101
|
|
|
76
102
|
if (primaryKeys.length === 1) {
|
|
77
103
|
const pk = primaryKeys[0];
|
|
78
|
-
if (pk.type === "number") {
|
|
104
|
+
if (pk.type === "number" && !pk.isUUID) {
|
|
79
105
|
const parsed = typeof idValue === "number" ? idValue : parseInt(String(idValue), 10);
|
|
80
106
|
if (isNaN(parsed)) {
|
|
81
107
|
throw new Error(`Invalid numeric ID: ${idValue}`);
|
|
@@ -96,7 +122,7 @@ export function parseIdValues(idValue: string | number, primaryKeys: { fieldName
|
|
|
96
122
|
for (let i = 0; i < primaryKeys.length; i++) {
|
|
97
123
|
const pk = primaryKeys[i];
|
|
98
124
|
const val = parts[i];
|
|
99
|
-
if (pk.type === "number") {
|
|
125
|
+
if (pk.type === "number" && !pk.isUUID) {
|
|
100
126
|
const parsed = parseInt(val, 10);
|
|
101
127
|
if (isNaN(parsed)) {
|
|
102
128
|
throw new Error(`Invalid numeric ID component: ${val}`);
|
|
@@ -110,7 +136,7 @@ export function parseIdValues(idValue: string | number, primaryKeys: { fieldName
|
|
|
110
136
|
return result;
|
|
111
137
|
}
|
|
112
138
|
|
|
113
|
-
export function buildCompositeId(values: Record<string,
|
|
139
|
+
export function buildCompositeId(values: Record<string, unknown>, primaryKeys: { fieldName: string; type: "string" | "number"; isUUID?: boolean }[]): string {
|
|
114
140
|
if (primaryKeys.length === 0) {
|
|
115
141
|
return "";
|
|
116
142
|
}
|
|
@@ -19,13 +19,13 @@ export * from "../interfaces";
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* EntityService - Facade for entity operations.
|
|
22
|
-
*
|
|
22
|
+
*
|
|
23
23
|
* This class provides a unified API for entity CRUD operations by delegating
|
|
24
24
|
* to specialized services:
|
|
25
25
|
* - EntityFetchService: Read operations (fetch, search, count)
|
|
26
26
|
* - EntityPersistService: Write operations (save, delete)
|
|
27
27
|
* - RelationService: Relation operations (fetch related, update relations)
|
|
28
|
-
*
|
|
28
|
+
*
|
|
29
29
|
* Implements the EntityRepository interface for database abstraction.
|
|
30
30
|
*/
|
|
31
31
|
export class EntityService implements EntityRepository {
|
|
@@ -44,7 +44,7 @@ export class EntityService implements EntityRepository {
|
|
|
44
44
|
/**
|
|
45
45
|
* Fetch a single entity by ID
|
|
46
46
|
*/
|
|
47
|
-
async fetchEntity<M extends Record<string,
|
|
47
|
+
async fetchEntity<M extends Record<string, unknown>>(
|
|
48
48
|
collectionPath: string,
|
|
49
49
|
entityId: string | number,
|
|
50
50
|
databaseId?: string
|
|
@@ -55,13 +55,14 @@ export class EntityService implements EntityRepository {
|
|
|
55
55
|
/**
|
|
56
56
|
* Fetch a collection of entities with optional filtering, ordering, and pagination
|
|
57
57
|
*/
|
|
58
|
-
async fetchCollection<M extends Record<string,
|
|
58
|
+
async fetchCollection<M extends Record<string, unknown>>(
|
|
59
59
|
collectionPath: string,
|
|
60
60
|
options: {
|
|
61
61
|
filter?: FilterValues<Extract<keyof M, string>>;
|
|
62
62
|
orderBy?: string;
|
|
63
63
|
order?: "desc" | "asc";
|
|
64
64
|
limit?: number;
|
|
65
|
+
offset?: number;
|
|
65
66
|
startAfter?: Record<string, unknown>;
|
|
66
67
|
searchString?: string;
|
|
67
68
|
databaseId?: string;
|
|
@@ -73,7 +74,7 @@ export class EntityService implements EntityRepository {
|
|
|
73
74
|
/**
|
|
74
75
|
* Search entities by text
|
|
75
76
|
*/
|
|
76
|
-
async searchEntities<M extends Record<string,
|
|
77
|
+
async searchEntities<M extends Record<string, unknown>>(
|
|
77
78
|
collectionPath: string,
|
|
78
79
|
searchString: string,
|
|
79
80
|
options: {
|
|
@@ -90,10 +91,11 @@ export class EntityService implements EntityRepository {
|
|
|
90
91
|
/**
|
|
91
92
|
* Count entities in a collection
|
|
92
93
|
*/
|
|
93
|
-
async countEntities<M extends Record<string,
|
|
94
|
+
async countEntities<M extends Record<string, unknown>>(
|
|
94
95
|
collectionPath: string,
|
|
95
96
|
options: {
|
|
96
97
|
filter?: FilterValues<Extract<keyof M, string>>;
|
|
98
|
+
searchString?: string;
|
|
97
99
|
databaseId?: string;
|
|
98
100
|
} = {}
|
|
99
101
|
): Promise<number> {
|
|
@@ -116,7 +118,7 @@ export class EntityService implements EntityRepository {
|
|
|
116
118
|
/**
|
|
117
119
|
* Fetch entities related to a parent entity
|
|
118
120
|
*/
|
|
119
|
-
async fetchRelatedEntities<M extends Record<string,
|
|
121
|
+
async fetchRelatedEntities<M extends Record<string, unknown>>(
|
|
120
122
|
parentCollectionPath: string,
|
|
121
123
|
parentEntityId: string | number,
|
|
122
124
|
relationKey: string,
|
|
@@ -145,7 +147,7 @@ export class EntityService implements EntityRepository {
|
|
|
145
147
|
/**
|
|
146
148
|
* Save an entity (create or update)
|
|
147
149
|
*/
|
|
148
|
-
async saveEntity<M extends Record<string,
|
|
150
|
+
async saveEntity<M extends Record<string, unknown>>(
|
|
149
151
|
collectionPath: string,
|
|
150
152
|
values: Partial<M>,
|
|
151
153
|
entityId?: string | number,
|
|
@@ -177,7 +179,7 @@ export class EntityService implements EntityRepository {
|
|
|
177
179
|
const result = await this.db.execute(sql.raw(sqlText));
|
|
178
180
|
const rows = result.rows;
|
|
179
181
|
if (process.env.NODE_ENV !== "production") {
|
|
180
|
-
console.debug(`SQL executed successfully. Returned ${Array.isArray(rows) ? rows.length :
|
|
182
|
+
console.debug(`SQL executed successfully. Returned ${Array.isArray(rows) ? rows.length : "non-array"} rows.`);
|
|
181
183
|
}
|
|
182
184
|
return rows as Record<string, unknown>[];
|
|
183
185
|
}
|
|
@@ -4,7 +4,7 @@ import { Client as PgClient } from "pg";
|
|
|
4
4
|
import { randomUUID } from "crypto";
|
|
5
5
|
import { EntityService } from "./entityService";
|
|
6
6
|
|
|
7
|
-
import { Entity, FetchCollectionProps, ListenCollectionProps, ListenEntityProps, DataDriver, CollectionUpdateMessage, EntityUpdateMessage, CollectionEntityPatchMessage, WebSocketMessage } from "@rebasepro/types";
|
|
7
|
+
import { Entity, FetchCollectionProps, ListenCollectionProps, ListenEntityProps, DataDriver, CollectionUpdateMessage, EntityUpdateMessage, CollectionEntityPatchMessage, WebSocketMessage, FilterValues, EntityCollection, RebaseCallContext } from "@rebasepro/types";
|
|
8
8
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
9
9
|
import { sql as drizzleSql } from "drizzle-orm";
|
|
10
10
|
import { RealtimeProvider, CollectionSubscriptionConfig, EntitySubscriptionConfig } from "../interfaces";
|
|
@@ -32,7 +32,7 @@ type RealTimeListenEntityProps = ListenEntityProps & { subscriptionId: string };
|
|
|
32
32
|
/**
|
|
33
33
|
* PostgreSQL-specific realtime service.
|
|
34
34
|
* Handles WebSocket connections and subscriptions for real-time entity updates.
|
|
35
|
-
*
|
|
35
|
+
*
|
|
36
36
|
* Implements the RealtimeProvider interface for database abstraction.
|
|
37
37
|
*/
|
|
38
38
|
export class RealtimeService extends EventEmitter implements RealtimeProvider {
|
|
@@ -50,6 +50,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
|
|
|
50
50
|
orderBy?: string;
|
|
51
51
|
order?: "desc" | "asc";
|
|
52
52
|
limit?: number;
|
|
53
|
+
offset?: number;
|
|
53
54
|
startAfter?: Record<string, unknown>;
|
|
54
55
|
databaseId?: string;
|
|
55
56
|
searchString?: string;
|
|
@@ -111,6 +112,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
|
|
|
111
112
|
orderBy?: string;
|
|
112
113
|
order?: "desc" | "asc";
|
|
113
114
|
limit?: number;
|
|
115
|
+
offset?: number;
|
|
114
116
|
startAfter?: Record<string, unknown>;
|
|
115
117
|
databaseId?: string;
|
|
116
118
|
searchString?: string;
|
|
@@ -253,6 +255,16 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
|
|
|
253
255
|
const subscriptionId = request.subscriptionId;
|
|
254
256
|
|
|
255
257
|
try {
|
|
258
|
+
// Early validation: ensure the requested collection exists in the registry
|
|
259
|
+
const collection = this.registry.getCollectionByPath(request.path);
|
|
260
|
+
if (!collection) {
|
|
261
|
+
const registered = this.registry.getCollections().map(c => c.slug).join(", ");
|
|
262
|
+
const msg = `Collection not found: '${request.path}'. Registered: [${registered}]`;
|
|
263
|
+
console.error(`[RealtimeService] ${msg}`);
|
|
264
|
+
this.sendError(clientId, msg, subscriptionId);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
256
268
|
// Store subscription with full request parameters and auth context for RLS
|
|
257
269
|
this._subscriptions.set(subscriptionId, {
|
|
258
270
|
clientId,
|
|
@@ -273,7 +285,6 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
|
|
|
273
285
|
// Send initial data
|
|
274
286
|
let entities;
|
|
275
287
|
if (this.driver) {
|
|
276
|
-
const collection = this.registry.getCollectionByPath(request.path);
|
|
277
288
|
entities = await this.driver.fetchCollection({
|
|
278
289
|
path: request.path,
|
|
279
290
|
collection: collection,
|
|
@@ -307,6 +318,16 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
|
|
|
307
318
|
const subscriptionId = request.subscriptionId;
|
|
308
319
|
|
|
309
320
|
try {
|
|
321
|
+
// Early validation: ensure the requested collection exists in the registry
|
|
322
|
+
const collection = this.registry.getCollectionByPath(request.path);
|
|
323
|
+
if (!collection) {
|
|
324
|
+
const registered = this.registry.getCollections().map(c => c.slug).join(", ");
|
|
325
|
+
const msg = `Collection not found: '${request.path}'. Registered: [${registered}]`;
|
|
326
|
+
console.error(`[RealtimeService] ${msg}`);
|
|
327
|
+
this.sendError(clientId, msg, subscriptionId);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
310
331
|
// Store subscription in memory with auth context for RLS
|
|
311
332
|
this._subscriptions.set(subscriptionId, {
|
|
312
333
|
clientId,
|
|
@@ -319,7 +340,6 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
|
|
|
319
340
|
// Send initial data
|
|
320
341
|
let entity;
|
|
321
342
|
if (this.driver) {
|
|
322
|
-
const collection = this.registry.getCollectionByPath(request.path);
|
|
323
343
|
entity = await this.driver.fetchEntity({
|
|
324
344
|
path: request.path,
|
|
325
345
|
entityId: request.entityId,
|
|
@@ -424,7 +444,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
|
|
|
424
444
|
try {
|
|
425
445
|
if (subscription.type === "entity" && notifyPath === originalPath) {
|
|
426
446
|
// Send entity update directly (only for exact path matches)
|
|
427
|
-
if (entity && (entity as
|
|
447
|
+
if (entity && (entity as unknown as Record<string, unknown>).values && ((entity as unknown as Record<string, unknown>).values as Record<string, unknown>)?._rebase_invalidated) {
|
|
428
448
|
this.debouncedEntityRefetch(subscriptionId, notifyPath, entityId, subscription);
|
|
429
449
|
} else {
|
|
430
450
|
this.sendEntityUpdate(subscription.clientId, subscriptionId, entity);
|
|
@@ -432,7 +452,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
|
|
|
432
452
|
} else if (subscription.type === "collection" && subscription.collectionRequest) {
|
|
433
453
|
// Phase 1: Send instant entity-level patch (no DB query)
|
|
434
454
|
// This gives immediate cross-tab feedback
|
|
435
|
-
if (!entity || !(entity as
|
|
455
|
+
if (!entity || !((entity as unknown as Record<string, unknown>).values && ((entity as unknown as Record<string, unknown>).values as Record<string, unknown>)?._rebase_invalidated)) {
|
|
436
456
|
this.sendCollectionEntityPatch(subscription.clientId, subscriptionId, entityId, entity);
|
|
437
457
|
}
|
|
438
458
|
|
|
@@ -453,7 +473,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
|
|
|
453
473
|
if (!callback) continue;
|
|
454
474
|
|
|
455
475
|
if (subscription.type === "entity" && notifyPath === originalPath) {
|
|
456
|
-
if (entity && (entity as
|
|
476
|
+
if (entity && (entity as unknown as Record<string, unknown>).values && ((entity as unknown as Record<string, unknown>).values as Record<string, unknown>)?._rebase_invalidated) {
|
|
457
477
|
this.debouncedEntityDriverRefetch(subscriptionId, notifyPath, entityId, subscription, callback);
|
|
458
478
|
} else {
|
|
459
479
|
// Call the callback directly with the entity (only for exact path matches)
|
|
@@ -476,7 +496,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
|
|
|
476
496
|
private debouncedCollectionRefetch(
|
|
477
497
|
subscriptionId: string,
|
|
478
498
|
notifyPath: string,
|
|
479
|
-
subscription: { clientId: string; collectionRequest?: { filter?: Record<string, unknown>; orderBy?: string; order?: "desc" | "asc"; limit?: number; startAfter?: Record<string, unknown>; databaseId?: string; searchString?: string }; authContext?: SubscriptionAuthContext }
|
|
499
|
+
subscription: { clientId: string; collectionRequest?: { filter?: Record<string, unknown>; orderBy?: string; order?: "desc" | "asc"; limit?: number; offset?: number; startAfter?: Record<string, unknown>; databaseId?: string; searchString?: string }; authContext?: SubscriptionAuthContext }
|
|
480
500
|
) {
|
|
481
501
|
const timerKey = `ws_${subscriptionId}`;
|
|
482
502
|
const existing = this.refetchTimers.get(timerKey);
|
|
@@ -502,7 +522,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
|
|
|
502
522
|
private debouncedDriverRefetch(
|
|
503
523
|
subscriptionId: string,
|
|
504
524
|
notifyPath: string,
|
|
505
|
-
subscription: { collectionRequest?: { filter?: Record<string, unknown>; orderBy?: string; order?: "desc" | "asc"; limit?: number; startAfter?: Record<string, unknown>; databaseId?: string; searchString?: string }; authContext?: SubscriptionAuthContext },
|
|
525
|
+
subscription: { collectionRequest?: { filter?: Record<string, unknown>; orderBy?: string; order?: "desc" | "asc"; limit?: number; offset?: number; startAfter?: Record<string, unknown>; databaseId?: string; searchString?: string }; authContext?: SubscriptionAuthContext },
|
|
506
526
|
callback: (data: Entity[] | Entity | null) => void
|
|
507
527
|
) {
|
|
508
528
|
const timerKey = `drv_${subscriptionId}`;
|
|
@@ -528,7 +548,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
|
|
|
528
548
|
*/
|
|
529
549
|
private async fetchCollectionWithAuth(
|
|
530
550
|
notifyPath: string,
|
|
531
|
-
collectionRequest: { filter?: Record<string, unknown>; orderBy?: string; order?: "desc" | "asc"; limit?: number; startAfter?: Record<string, unknown>; databaseId?: string; searchString?: string },
|
|
551
|
+
collectionRequest: { filter?: Record<string, unknown>; orderBy?: string; order?: "desc" | "asc"; limit?: number; offset?: number; startAfter?: Record<string, unknown>; databaseId?: string; searchString?: string },
|
|
532
552
|
authContext?: SubscriptionAuthContext
|
|
533
553
|
): Promise<Entity[]> {
|
|
534
554
|
if (this.driver) {
|
|
@@ -540,6 +560,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
|
|
|
540
560
|
orderBy: collectionRequest.orderBy,
|
|
541
561
|
order: collectionRequest.order,
|
|
542
562
|
limit: collectionRequest.limit,
|
|
563
|
+
offset: collectionRequest.offset,
|
|
543
564
|
startAfter: collectionRequest.startAfter,
|
|
544
565
|
searchString: collectionRequest.searchString
|
|
545
566
|
});
|
|
@@ -549,7 +570,8 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
|
|
|
549
570
|
return await this.db.transaction(async (tx) => {
|
|
550
571
|
await tx.execute(drizzleSql`SELECT set_config('app.user_id', ${authContext.userId}, true)`);
|
|
551
572
|
await tx.execute(drizzleSql`SELECT set_config('app.user_roles', ${authContext.roles.join(",")}, true)`);
|
|
552
|
-
await tx.execute(drizzleSql`SELECT set_config('app.jwt', ${JSON.stringify({ sub: authContext.userId,
|
|
573
|
+
await tx.execute(drizzleSql`SELECT set_config('app.jwt', ${JSON.stringify({ sub: authContext.userId,
|
|
574
|
+
roles: authContext.roles })}, true)`);
|
|
553
575
|
const txEntityService = new EntityService(tx, this.registry);
|
|
554
576
|
let fetchedEntities;
|
|
555
577
|
if (collectionRequest.searchString) {
|
|
@@ -557,7 +579,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
|
|
|
557
579
|
notifyPath,
|
|
558
580
|
collectionRequest.searchString,
|
|
559
581
|
{
|
|
560
|
-
filter: collectionRequest.filter as
|
|
582
|
+
filter: collectionRequest.filter as FilterValues<string>,
|
|
561
583
|
orderBy: collectionRequest.orderBy,
|
|
562
584
|
order: collectionRequest.order,
|
|
563
585
|
limit: collectionRequest.limit,
|
|
@@ -566,10 +588,11 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
|
|
|
566
588
|
);
|
|
567
589
|
} else {
|
|
568
590
|
fetchedEntities = await txEntityService.fetchCollection(notifyPath, {
|
|
569
|
-
filter: collectionRequest.filter as
|
|
591
|
+
filter: collectionRequest.filter as FilterValues<string>,
|
|
570
592
|
orderBy: collectionRequest.orderBy,
|
|
571
593
|
order: collectionRequest.order,
|
|
572
594
|
limit: collectionRequest.limit,
|
|
595
|
+
offset: collectionRequest.offset,
|
|
573
596
|
startAfter: collectionRequest.startAfter,
|
|
574
597
|
databaseId: collectionRequest.databaseId
|
|
575
598
|
});
|
|
@@ -578,18 +601,20 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
|
|
|
578
601
|
// Re-apply `afterRead` lifecycle hooks to ensure consistent data structures
|
|
579
602
|
// between the initial driver fetch and this RLS-bound refetch.
|
|
580
603
|
const registryCollection = this.registry.getCollectionByPath(notifyPath);
|
|
581
|
-
const resolvedCollection = collection ? { ...collection,
|
|
582
|
-
|
|
604
|
+
const resolvedCollection = collection ? { ...collection,
|
|
605
|
+
...registryCollection } as EntityCollection : registryCollection as EntityCollection;
|
|
606
|
+
|
|
583
607
|
const callbacks = resolvedCollection?.callbacks;
|
|
584
608
|
const propertyCallbacks = resolvedCollection?.properties ? buildPropertyCallbacks(resolvedCollection.properties) : undefined;
|
|
585
609
|
|
|
586
610
|
if (callbacks?.afterRead || propertyCallbacks?.afterRead) {
|
|
587
611
|
const contextForCallback = {
|
|
588
|
-
user: { uid: authContext.userId,
|
|
612
|
+
user: { uid: authContext.userId,
|
|
613
|
+
roles: authContext.roles },
|
|
589
614
|
driver: this.driver,
|
|
590
|
-
data: this.driver ? (this.driver as
|
|
591
|
-
} as
|
|
592
|
-
|
|
615
|
+
data: this.driver ? (this.driver as unknown as Record<string, unknown>).data : undefined
|
|
616
|
+
} as unknown as RebaseCallContext;
|
|
617
|
+
|
|
593
618
|
return await Promise.all(fetchedEntities.map(async (entity) => {
|
|
594
619
|
let processedEntity = entity;
|
|
595
620
|
if (callbacks?.afterRead) {
|
|
@@ -625,7 +650,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
|
|
|
625
650
|
notifyPath,
|
|
626
651
|
collectionRequest.searchString,
|
|
627
652
|
{
|
|
628
|
-
filter: collectionRequest.filter as
|
|
653
|
+
filter: collectionRequest.filter as FilterValues<string>,
|
|
629
654
|
orderBy: collectionRequest.orderBy,
|
|
630
655
|
order: collectionRequest.order,
|
|
631
656
|
limit: collectionRequest.limit,
|
|
@@ -634,10 +659,11 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
|
|
|
634
659
|
);
|
|
635
660
|
}
|
|
636
661
|
return await this.entityService.fetchCollection(notifyPath, {
|
|
637
|
-
filter: collectionRequest.filter as
|
|
662
|
+
filter: collectionRequest.filter as FilterValues<string>,
|
|
638
663
|
orderBy: collectionRequest.orderBy,
|
|
639
664
|
order: collectionRequest.order,
|
|
640
665
|
limit: collectionRequest.limit,
|
|
666
|
+
offset: collectionRequest.offset,
|
|
641
667
|
startAfter: collectionRequest.startAfter,
|
|
642
668
|
databaseId: collectionRequest.databaseId
|
|
643
669
|
});
|
|
@@ -677,7 +703,7 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
|
|
|
677
703
|
notifyPath: string,
|
|
678
704
|
entityId: string,
|
|
679
705
|
subscription: { clientId: string; authContext?: SubscriptionAuthContext },
|
|
680
|
-
callback: (data:
|
|
706
|
+
callback: (data: Entity[] | Entity | null) => void
|
|
681
707
|
) {
|
|
682
708
|
const timerKey = `drve_${subscriptionId}`;
|
|
683
709
|
const existing = this.refetchTimers.get(timerKey);
|
|
@@ -716,24 +742,27 @@ export class RealtimeService extends EventEmitter implements RealtimeProvider {
|
|
|
716
742
|
return await this.db.transaction(async (tx) => {
|
|
717
743
|
await tx.execute(drizzleSql`SELECT set_config('app.user_id', ${authContext.userId}, true)`);
|
|
718
744
|
await tx.execute(drizzleSql`SELECT set_config('app.user_roles', ${authContext.roles.join(",")}, true)`);
|
|
719
|
-
await tx.execute(drizzleSql`SELECT set_config('app.jwt', ${JSON.stringify({ sub: authContext.userId,
|
|
745
|
+
await tx.execute(drizzleSql`SELECT set_config('app.jwt', ${JSON.stringify({ sub: authContext.userId,
|
|
746
|
+
roles: authContext.roles })}, true)`);
|
|
720
747
|
const txEntityService = new EntityService(tx, this.registry);
|
|
721
748
|
let processedEntity = await txEntityService.fetchEntity(notifyPath, entityId, collection?.databaseId);
|
|
722
749
|
|
|
723
750
|
if (processedEntity) {
|
|
724
751
|
const registryCollection = this.registry.getCollectionByPath(notifyPath);
|
|
725
|
-
const resolvedCollection = collection ? { ...collection,
|
|
726
|
-
|
|
752
|
+
const resolvedCollection = collection ? { ...collection,
|
|
753
|
+
...registryCollection } as EntityCollection : registryCollection as EntityCollection;
|
|
754
|
+
|
|
727
755
|
const callbacks = resolvedCollection?.callbacks;
|
|
728
756
|
const propertyCallbacks = resolvedCollection?.properties ? buildPropertyCallbacks(resolvedCollection.properties) : undefined;
|
|
729
757
|
|
|
730
758
|
if (callbacks?.afterRead || propertyCallbacks?.afterRead) {
|
|
731
759
|
const contextForCallback = {
|
|
732
|
-
user: { uid: authContext.userId,
|
|
760
|
+
user: { uid: authContext.userId,
|
|
761
|
+
roles: authContext.roles },
|
|
733
762
|
driver: this.driver,
|
|
734
|
-
data: this.driver ? (this.driver as
|
|
735
|
-
} as
|
|
736
|
-
|
|
763
|
+
data: this.driver ? (this.driver as unknown as Record<string, unknown>).data : undefined
|
|
764
|
+
} as unknown as RebaseCallContext;
|
|
765
|
+
|
|
737
766
|
if (callbacks?.afterRead) {
|
|
738
767
|
processedEntity = await callbacks.afterRead({
|
|
739
768
|
collection: resolvedCollection,
|