@rebasepro/server-postgresql 0.4.0 → 0.6.0
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 +69 -89
- package/dist/{server-postgresql/src/PostgresAdapter.d.ts → PostgresAdapter.d.ts} +1 -1
- package/dist/{server-postgresql/src/PostgresBackendDriver.d.ts → PostgresBackendDriver.d.ts} +2 -2
- package/dist/{server-postgresql/src/PostgresBootstrapper.d.ts → PostgresBootstrapper.d.ts} +11 -1
- package/dist/{server-postgresql/src/auth → auth}/services.d.ts +11 -11
- package/dist/{server-postgresql/src/collections → collections}/PostgresCollectionRegistry.d.ts +4 -0
- package/dist/{server-postgresql/src/data-transformer.d.ts → data-transformer.d.ts} +0 -3
- package/dist/{server-postgresql/src/databasePoolManager.d.ts → databasePoolManager.d.ts} +1 -1
- package/dist/index.es.js +10174 -11184
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +10735 -11462
- package/dist/index.umd.js.map +1 -1
- package/dist/{server-postgresql/src/services → services}/EntityPersistService.d.ts +0 -14
- package/dist/types.d.ts +3 -0
- package/dist/utils/pg-error-utils.d.ts +55 -0
- package/dist/{server-postgresql/src/websocket.d.ts → websocket.d.ts} +8 -3
- package/package.json +24 -21
- package/src/PostgresAdapter.ts +9 -10
- package/src/PostgresBackendDriver.ts +135 -122
- package/src/PostgresBootstrapper.ts +90 -16
- package/src/auth/ensure-tables.ts +28 -5
- package/src/auth/services.ts +56 -45
- package/src/cli.ts +140 -110
- package/src/collections/PostgresCollectionRegistry.ts +7 -0
- package/src/connection.ts +11 -6
- package/src/data-transformer.ts +73 -109
- package/src/databasePoolManager.ts +5 -3
- package/src/history/HistoryService.ts +3 -2
- package/src/history/ensure-history-table.ts +5 -4
- package/src/schema/auth-schema.ts +1 -2
- package/src/schema/doctor-cli.ts +2 -1
- package/src/schema/doctor.ts +40 -37
- package/src/schema/generate-drizzle-schema-logic.ts +56 -18
- package/src/schema/generate-drizzle-schema.ts +11 -11
- package/src/schema/introspect-db-inference.ts +25 -25
- package/src/schema/introspect-db-logic.ts +38 -38
- package/src/schema/introspect-db.ts +28 -27
- package/src/services/BranchService.ts +14 -0
- package/src/services/EntityFetchService.ts +28 -25
- package/src/services/EntityPersistService.ts +11 -124
- package/src/services/RelationService.ts +57 -37
- package/src/services/entity-helpers.ts +6 -2
- package/src/services/realtimeService.ts +45 -32
- package/src/types.ts +4 -0
- package/src/utils/drizzle-conditions.ts +31 -15
- package/src/utils/pg-error-utils.ts +211 -0
- package/src/websocket.ts +51 -33
- package/test/auth-services.test.ts +36 -19
- package/test/batch-many-to-many-regression.test.ts +119 -39
- package/test/data-transformer-hardening.test.ts +67 -33
- package/test/data-transformer.test.ts +4 -2
- package/test/doctor.test.ts +10 -5
- package/test/drizzle-conditions.test.ts +59 -6
- package/test/generate-drizzle-schema.test.ts +65 -40
- package/test/introspect-db-generation.test.ts +179 -81
- package/test/introspect-db-utils.test.ts +92 -37
- package/test/mocks/chalk.cjs +7 -0
- package/test/pg-error-utils.test.ts +221 -0
- package/test/postgresDataDriver.test.ts +14 -5
- package/test/property-ordering.test.ts +126 -79
- package/test/realtimeService.test.ts +6 -2
- package/test/relation-pipeline-gaps.test.ts +84 -36
- package/test/relations.test.ts +247 -0
- package/test/unmapped-tables-safety.test.ts +14 -6
- package/test/websocket.test.ts +1 -1
- package/tsconfig.json +5 -0
- package/tsconfig.prod.json +3 -0
- package/vite.config.ts +5 -5
- package/dist/common/src/collections/CollectionRegistry.d.ts +0 -56
- package/dist/common/src/collections/default-collections.d.ts +0 -9
- package/dist/common/src/collections/index.d.ts +0 -2
- package/dist/common/src/data/buildRebaseData.d.ts +0 -14
- package/dist/common/src/data/query_builder.d.ts +0 -55
- package/dist/common/src/index.d.ts +0 -4
- package/dist/common/src/util/builders.d.ts +0 -57
- package/dist/common/src/util/callbacks.d.ts +0 -6
- package/dist/common/src/util/collections.d.ts +0 -11
- package/dist/common/src/util/common.d.ts +0 -2
- package/dist/common/src/util/conditions.d.ts +0 -26
- package/dist/common/src/util/entities.d.ts +0 -58
- package/dist/common/src/util/enums.d.ts +0 -3
- package/dist/common/src/util/index.d.ts +0 -16
- package/dist/common/src/util/navigation_from_path.d.ts +0 -34
- package/dist/common/src/util/navigation_utils.d.ts +0 -20
- package/dist/common/src/util/parent_references_from_path.d.ts +0 -6
- package/dist/common/src/util/paths.d.ts +0 -14
- package/dist/common/src/util/permissions.d.ts +0 -6
- package/dist/common/src/util/references.d.ts +0 -2
- package/dist/common/src/util/relations.d.ts +0 -22
- package/dist/common/src/util/resolutions.d.ts +0 -72
- package/dist/common/src/util/storage.d.ts +0 -24
- package/dist/types/src/controllers/analytics_controller.d.ts +0 -7
- package/dist/types/src/controllers/auth.d.ts +0 -104
- package/dist/types/src/controllers/client.d.ts +0 -168
- package/dist/types/src/controllers/collection_registry.d.ts +0 -46
- package/dist/types/src/controllers/customization_controller.d.ts +0 -60
- package/dist/types/src/controllers/data.d.ts +0 -207
- package/dist/types/src/controllers/data_driver.d.ts +0 -218
- package/dist/types/src/controllers/database_admin.d.ts +0 -11
- package/dist/types/src/controllers/dialogs_controller.d.ts +0 -36
- package/dist/types/src/controllers/effective_role.d.ts +0 -4
- package/dist/types/src/controllers/email.d.ts +0 -36
- package/dist/types/src/controllers/index.d.ts +0 -18
- package/dist/types/src/controllers/local_config_persistence.d.ts +0 -20
- package/dist/types/src/controllers/navigation.d.ts +0 -225
- package/dist/types/src/controllers/registry.d.ts +0 -63
- package/dist/types/src/controllers/side_dialogs_controller.d.ts +0 -67
- package/dist/types/src/controllers/side_entity_controller.d.ts +0 -97
- package/dist/types/src/controllers/snackbar.d.ts +0 -24
- package/dist/types/src/controllers/storage.d.ts +0 -171
- package/dist/types/src/index.d.ts +0 -4
- package/dist/types/src/rebase_context.d.ts +0 -122
- package/dist/types/src/types/auth_adapter.d.ts +0 -301
- package/dist/types/src/types/backend.d.ts +0 -536
- package/dist/types/src/types/backend_hooks.d.ts +0 -172
- package/dist/types/src/types/builders.d.ts +0 -15
- package/dist/types/src/types/chips.d.ts +0 -5
- package/dist/types/src/types/collections.d.ts +0 -941
- package/dist/types/src/types/component_ref.d.ts +0 -47
- package/dist/types/src/types/cron.d.ts +0 -102
- package/dist/types/src/types/data_source.d.ts +0 -64
- package/dist/types/src/types/database_adapter.d.ts +0 -94
- package/dist/types/src/types/entities.d.ts +0 -145
- package/dist/types/src/types/entity_actions.d.ts +0 -104
- package/dist/types/src/types/entity_callbacks.d.ts +0 -173
- package/dist/types/src/types/entity_link_builder.d.ts +0 -7
- package/dist/types/src/types/entity_overrides.d.ts +0 -10
- package/dist/types/src/types/entity_views.d.ts +0 -87
- package/dist/types/src/types/export_import.d.ts +0 -21
- package/dist/types/src/types/formex.d.ts +0 -40
- package/dist/types/src/types/index.d.ts +0 -28
- package/dist/types/src/types/locales.d.ts +0 -4
- package/dist/types/src/types/modify_collections.d.ts +0 -5
- package/dist/types/src/types/plugins.d.ts +0 -282
- package/dist/types/src/types/properties.d.ts +0 -1181
- package/dist/types/src/types/property_config.d.ts +0 -74
- package/dist/types/src/types/relations.d.ts +0 -336
- package/dist/types/src/types/slots.d.ts +0 -262
- package/dist/types/src/types/translations.d.ts +0 -900
- package/dist/types/src/types/user_management_delegate.d.ts +0 -86
- package/dist/types/src/types/websockets.d.ts +0 -78
- package/dist/types/src/users/index.d.ts +0 -1
- package/dist/types/src/users/user.d.ts +0 -50
- package/drizzle.test.config.ts +0 -10
- /package/dist/{server-postgresql/src/auth → auth}/ensure-tables.d.ts +0 -0
- /package/dist/{server-postgresql/src/cli.d.ts → cli.d.ts} +0 -0
- /package/dist/{server-postgresql/src/connection.d.ts → connection.d.ts} +0 -0
- /package/dist/{server-postgresql/src/history → history}/HistoryService.d.ts +0 -0
- /package/dist/{server-postgresql/src/history → history}/ensure-history-table.d.ts +0 -0
- /package/dist/{server-postgresql/src/index.d.ts → index.d.ts} +0 -0
- /package/dist/{server-postgresql/src/interfaces.d.ts → interfaces.d.ts} +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/auth-schema.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/doctor-cli.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/doctor.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/generate-drizzle-schema-logic.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/generate-drizzle-schema.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/introspect-db-inference.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/introspect-db-logic.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/introspect-db.d.ts +0 -0
- /package/dist/{server-postgresql/src/schema → schema}/test-schema.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/BranchService.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/EntityFetchService.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/RelationService.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/entity-helpers.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/entityService.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/index.d.ts +0 -0
- /package/dist/{server-postgresql/src/services → services}/realtimeService.d.ts +0 -0
- /package/dist/{server-postgresql/src/utils → utils}/drizzle-conditions.d.ts +0 -0
|
@@ -4,6 +4,8 @@ import { FilterValues, WhereFilterOp, Relation, JoinStep, LogicalCondition, Filt
|
|
|
4
4
|
import { getColumnName, resolveCollectionRelations } from "@rebasepro/common";
|
|
5
5
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
6
6
|
import { ConditionBuilderStatic } from "../interfaces";
|
|
7
|
+
import { logger } from "@rebasepro/server-core";
|
|
8
|
+
import { getColumnMeta } from "../services/entity-helpers";
|
|
7
9
|
|
|
8
10
|
/** Drizzle dynamic query builder — accepts innerJoin + where chaining */
|
|
9
11
|
|
|
@@ -48,7 +50,7 @@ export class DrizzleConditionBuilder {
|
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
if (!fieldColumn) {
|
|
51
|
-
|
|
53
|
+
logger.warn(`Filtering by field '${field}', but it does not exist in table for collection '${collectionPath}'`);
|
|
52
54
|
continue;
|
|
53
55
|
}
|
|
54
56
|
|
|
@@ -90,7 +92,7 @@ export class DrizzleConditionBuilder {
|
|
|
90
92
|
}
|
|
91
93
|
}
|
|
92
94
|
if (!fieldColumn) {
|
|
93
|
-
|
|
95
|
+
logger.warn(`Filtering by field '${cond.column}', but it does not exist in table for collection '${collectionPath}'`);
|
|
94
96
|
return null;
|
|
95
97
|
}
|
|
96
98
|
return this.buildSingleFilterCondition(fieldColumn, cond.operator as WhereFilterOp, cond.value);
|
|
@@ -129,25 +131,39 @@ export class DrizzleConditionBuilder {
|
|
|
129
131
|
return inArray(column, value);
|
|
130
132
|
}
|
|
131
133
|
return null;
|
|
132
|
-
case "array-contains":
|
|
134
|
+
case "array-contains": {
|
|
135
|
+
const meta = getColumnMeta(column);
|
|
136
|
+
if (meta.dataType === "array" || meta.columnType === "PgArray") {
|
|
137
|
+
return sql`${column} @> ARRAY[${value}]`;
|
|
138
|
+
}
|
|
133
139
|
// For JSONB arrays: checks if the column contains the given value
|
|
134
140
|
return sql`${column} @> ${JSON.stringify([value])}`;
|
|
135
|
-
|
|
136
|
-
|
|
141
|
+
}
|
|
142
|
+
case "array-contains-any": {
|
|
143
|
+
const meta = getColumnMeta(column);
|
|
144
|
+
const isNativeArray = meta.dataType === "array" || meta.columnType === "PgArray";
|
|
137
145
|
if (Array.isArray(value) && value.length > 0) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
146
|
+
if (isNativeArray) {
|
|
147
|
+
return sql`${column} && ARRAY[${sql.join(value.map(v => sql`${v}`), sql`, `)}]`;
|
|
148
|
+
} else {
|
|
149
|
+
// Use the ?| operator for JSONB overlap with text array
|
|
150
|
+
const textValues = value.map(v => String(v));
|
|
151
|
+
return sql`${column} ?| array[${sql.join(textValues.map(v => sql`${v}`), sql`, `)}]`;
|
|
152
|
+
}
|
|
141
153
|
}
|
|
142
154
|
// Single value fallback: treat as array-contains
|
|
155
|
+
if (isNativeArray) {
|
|
156
|
+
return sql`${column} @> ARRAY[${value}]`;
|
|
157
|
+
}
|
|
143
158
|
return sql`${column} @> ${JSON.stringify([value])}`;
|
|
159
|
+
}
|
|
144
160
|
case "not-in":
|
|
145
161
|
if (Array.isArray(value) && value.length > 0) {
|
|
146
162
|
return sql`${column} NOT IN (${sql.join(value.map(v => sql`${v}`), sql`, `)})`;
|
|
147
163
|
}
|
|
148
164
|
return null;
|
|
149
165
|
default:
|
|
150
|
-
|
|
166
|
+
logger.warn(`Unsupported filter operation: ${op}`);
|
|
151
167
|
return null;
|
|
152
168
|
}
|
|
153
169
|
}
|
|
@@ -247,7 +263,7 @@ export class DrizzleConditionBuilder {
|
|
|
247
263
|
);
|
|
248
264
|
whereConditions.push(simpleCondition);
|
|
249
265
|
} else {
|
|
250
|
-
|
|
266
|
+
logger.error("🔍 [buildRelationConditions] Failed to find junction table info and no foreign key specified");
|
|
251
267
|
throw new Error(`Cannot resolve inverse many relation '${relation.relationName}'. Either specify 'through' property, ensure corresponding owning relation exists with junction table configuration, or specify 'foreignKeyOnTarget' for one-to-many relationships.`);
|
|
252
268
|
}
|
|
253
269
|
} else {
|
|
@@ -711,9 +727,9 @@ export class DrizzleConditionBuilder {
|
|
|
711
727
|
const fieldColumn = table[key as keyof typeof table] as AnyPgColumn;
|
|
712
728
|
if (fieldColumn) {
|
|
713
729
|
// Verify that the underlying database column supports string pattern-matching
|
|
714
|
-
const supportsILike =
|
|
715
|
-
fieldColumn instanceof PgVarchar ||
|
|
716
|
-
fieldColumn instanceof PgText ||
|
|
730
|
+
const supportsILike =
|
|
731
|
+
fieldColumn instanceof PgVarchar ||
|
|
732
|
+
fieldColumn instanceof PgText ||
|
|
717
733
|
fieldColumn instanceof PgChar ||
|
|
718
734
|
(fieldColumn && typeof fieldColumn === "object" && !("columnType" in fieldColumn));
|
|
719
735
|
if (supportsILike) {
|
|
@@ -1058,7 +1074,7 @@ export class DrizzleConditionBuilder {
|
|
|
1058
1074
|
console.debug("🔍 [findCorrespondingJunctionTable] Returning junction info:", result);
|
|
1059
1075
|
return result;
|
|
1060
1076
|
} catch (error) {
|
|
1061
|
-
|
|
1077
|
+
logger.error(`🔍 [findCorrespondingJunctionTable] Error finding corresponding junction table for relation '${relation.relationName}'`, { error: error });
|
|
1062
1078
|
return null;
|
|
1063
1079
|
}
|
|
1064
1080
|
}
|
|
@@ -1108,7 +1124,7 @@ export class DrizzleConditionBuilder {
|
|
|
1108
1124
|
filter: vectorSearch.threshold != null
|
|
1109
1125
|
? sql`(${column} ${sql.raw(operator)} ${sql.raw(vectorLiteral)}) < ${vectorSearch.threshold}`
|
|
1110
1126
|
: undefined,
|
|
1111
|
-
distanceSelect: sql`(${column} ${sql.raw(operator)} ${sql.raw(vectorLiteral)})
|
|
1127
|
+
distanceSelect: sql`(${column} ${sql.raw(operator)} ${sql.raw(vectorLiteral)})`
|
|
1112
1128
|
};
|
|
1113
1129
|
}
|
|
1114
1130
|
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared PostgreSQL error extraction and user-friendly message formatting.
|
|
3
|
+
*
|
|
4
|
+
* Drizzle wraps native PG errors in a `.cause` chain. These utilities
|
|
5
|
+
* unwrap that chain to get the real PostgreSQL error (identified by a
|
|
6
|
+
* 5-character alphanumeric `code` such as `42P01`) and translate it into
|
|
7
|
+
* a message that is safe and helpful to show to end-users.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { logger } from "@rebasepro/server-core";
|
|
11
|
+
|
|
12
|
+
/** Shape of PostgreSQL errors with diagnostic metadata. */
|
|
13
|
+
export interface PostgresError extends Error {
|
|
14
|
+
code?: string;
|
|
15
|
+
detail?: string;
|
|
16
|
+
hint?: string;
|
|
17
|
+
constraint?: string;
|
|
18
|
+
column?: string;
|
|
19
|
+
table?: string;
|
|
20
|
+
dataType?: string;
|
|
21
|
+
cause?: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Extract the underlying PostgreSQL error from a Drizzle wrapper.
|
|
26
|
+
* Drizzle wraps PG errors in a `cause` property — this function
|
|
27
|
+
* recursively walks the chain until it finds an object with a PG
|
|
28
|
+
* error code (5-char alphanumeric, e.g. `42P01`).
|
|
29
|
+
*/
|
|
30
|
+
export function extractPgError(error: unknown): PostgresError | null {
|
|
31
|
+
if (!error || typeof error !== "object") return null;
|
|
32
|
+
if (!(error instanceof Error)) {
|
|
33
|
+
// Check non-Error objects for a cause chain (Drizzle sometimes wraps oddly)
|
|
34
|
+
if ("cause" in error && (error as Record<string, unknown>).cause && typeof (error as Record<string, unknown>).cause === "object") {
|
|
35
|
+
return extractPgError((error as Record<string, unknown>).cause);
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check if the error itself has a PG error code
|
|
41
|
+
if ("code" in error && typeof (error as PostgresError).code === "string" && /^[0-9A-Z]{5}$/.test((error as PostgresError).code!)) {
|
|
42
|
+
return error as PostgresError;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check the cause chain (Drizzle wraps PG errors)
|
|
46
|
+
if (error.cause && typeof error.cause === "object") {
|
|
47
|
+
return extractPgError(error.cause);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Walk the error cause chain and return the deepest meaningful message.
|
|
55
|
+
*/
|
|
56
|
+
export function extractCauseMessage(error: unknown): string | null {
|
|
57
|
+
if (!error || typeof error !== "object") return null;
|
|
58
|
+
if (!(error instanceof Error)) return null;
|
|
59
|
+
|
|
60
|
+
if (error.cause && typeof error.cause === "object") {
|
|
61
|
+
const deeper = extractCauseMessage(error.cause);
|
|
62
|
+
if (deeper) return deeper;
|
|
63
|
+
// The cause itself has a message
|
|
64
|
+
if (error.cause instanceof Error && error.cause.message) {
|
|
65
|
+
return error.cause.message;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Translate a raw PostgreSQL error into a user-friendly message.
|
|
73
|
+
*
|
|
74
|
+
* @param pgError - The extracted PostgreSQL error (from {@link extractPgError})
|
|
75
|
+
* @param context - A human-readable context string (e.g. collection slug or path)
|
|
76
|
+
* @returns An object with a `message` safe for the client and the PG `code`.
|
|
77
|
+
*/
|
|
78
|
+
export function pgErrorToFriendlyMessage(pgError: PostgresError, context: string): { message: string; code: string } {
|
|
79
|
+
const detail = pgError.detail as string | undefined;
|
|
80
|
+
const hint = pgError.hint as string | undefined;
|
|
81
|
+
const constraint = pgError.constraint as string | undefined;
|
|
82
|
+
const column = pgError.column as string | undefined;
|
|
83
|
+
const table = pgError.table as string | undefined;
|
|
84
|
+
const dataType = pgError.dataType as string | undefined;
|
|
85
|
+
const pgMessage = pgError.message || "Unknown database error";
|
|
86
|
+
const code = pgError.code || "UNKNOWN";
|
|
87
|
+
|
|
88
|
+
const suffix = hint ? ` Hint: ${hint}` : "";
|
|
89
|
+
const tableRef = table ?? context;
|
|
90
|
+
|
|
91
|
+
switch (pgError.code) {
|
|
92
|
+
case "23503": // foreign_key_violation
|
|
93
|
+
return {
|
|
94
|
+
message: detail
|
|
95
|
+
? `Foreign key constraint violated: ${detail}${suffix}`
|
|
96
|
+
: `Cannot complete operation: a foreign key constraint${constraint ? ` (${constraint})` : ""} was violated in "${context}".${suffix}`,
|
|
97
|
+
code
|
|
98
|
+
};
|
|
99
|
+
case "23505": // unique_violation
|
|
100
|
+
return {
|
|
101
|
+
message: detail
|
|
102
|
+
? `Duplicate value: ${detail}${suffix}`
|
|
103
|
+
: `Cannot complete operation: a unique constraint${constraint ? ` (${constraint})` : ""} was violated in "${context}".${suffix}`,
|
|
104
|
+
code
|
|
105
|
+
};
|
|
106
|
+
case "23502": // not_null_violation
|
|
107
|
+
return {
|
|
108
|
+
message: `Missing required field: "${column ?? "unknown"}" in "${tableRef}" cannot be empty.${suffix}`,
|
|
109
|
+
code
|
|
110
|
+
};
|
|
111
|
+
case "23514": // check_violation
|
|
112
|
+
return {
|
|
113
|
+
message: `Validation failed: a check constraint${constraint ? ` (${constraint})` : ""} was violated in "${context}".${suffix}`,
|
|
114
|
+
code
|
|
115
|
+
};
|
|
116
|
+
case "22P02": // invalid_text_representation (e.g. invalid UUID, wrong enum value)
|
|
117
|
+
return {
|
|
118
|
+
message: `Invalid data format in "${context}": ${pgMessage}${suffix}`,
|
|
119
|
+
code
|
|
120
|
+
};
|
|
121
|
+
case "22001": // string_data_right_truncation (value too long)
|
|
122
|
+
return {
|
|
123
|
+
message: `Value too long for column "${column ?? "unknown"}" in "${tableRef}": ${pgMessage}${suffix}`,
|
|
124
|
+
code
|
|
125
|
+
};
|
|
126
|
+
case "22003": // numeric_value_out_of_range
|
|
127
|
+
return {
|
|
128
|
+
message: `Numeric value out of range for column "${column ?? "unknown"}" in "${tableRef}": ${pgMessage}${suffix}`,
|
|
129
|
+
code
|
|
130
|
+
};
|
|
131
|
+
case "42703": // undefined_column
|
|
132
|
+
return {
|
|
133
|
+
message: `Unknown column in "${tableRef}": ${pgMessage}. Check if your schema is up to date (run migrations).${suffix}`,
|
|
134
|
+
code
|
|
135
|
+
};
|
|
136
|
+
case "42P01": // undefined_table
|
|
137
|
+
return {
|
|
138
|
+
message: `Table not found for "${context}": ${pgMessage}. Check if your schema is up to date (run migrations).${suffix}`,
|
|
139
|
+
code
|
|
140
|
+
};
|
|
141
|
+
case "42501": // insufficient_privilege
|
|
142
|
+
return {
|
|
143
|
+
message: `Permission denied on "${tableRef}". Check your database credentials and RLS policies.${suffix}`,
|
|
144
|
+
code
|
|
145
|
+
};
|
|
146
|
+
case "28000": // invalid_authorization_specification
|
|
147
|
+
return {
|
|
148
|
+
message: `Authorization failed for "${context}". Check your database credentials.${suffix}`,
|
|
149
|
+
code
|
|
150
|
+
};
|
|
151
|
+
default: {
|
|
152
|
+
// Unhandled PG code — still surface the actual database message
|
|
153
|
+
const parts = [`Database error in "${context}" [${code}]: ${pgMessage}`];
|
|
154
|
+
if (detail) parts.push(`Detail: ${detail}`);
|
|
155
|
+
if (column) parts.push(`Column: ${column}`);
|
|
156
|
+
if (dataType) parts.push(`Data type: ${dataType}`);
|
|
157
|
+
if (constraint) parts.push(`Constraint: ${constraint}`);
|
|
158
|
+
if (hint) parts.push(`Hint: ${hint}`);
|
|
159
|
+
return { message: parts.join(". "), code };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Sanitize any error into a message safe and helpful for the client.
|
|
166
|
+
*
|
|
167
|
+
* Extracts the PG error from the Drizzle cause chain when possible;
|
|
168
|
+
* falls back to a generic message that doesn't leak SQL.
|
|
169
|
+
*
|
|
170
|
+
* @param error - The raw caught error
|
|
171
|
+
* @param context - A human-readable context string (e.g. collection path)
|
|
172
|
+
* @returns An object with `message` (user-friendly) and optional `code` (PG code).
|
|
173
|
+
*/
|
|
174
|
+
export function sanitizeErrorForClient(error: unknown, context: string): { message: string; code?: string } {
|
|
175
|
+
// ── Always log the full, unsanitized error server-side ──────────
|
|
176
|
+
const pgError = extractPgError(error);
|
|
177
|
+
|
|
178
|
+
if (pgError) {
|
|
179
|
+
logger.error(`[PG ${pgError.code}] Error in "${context}"`, {
|
|
180
|
+
code: pgError.code,
|
|
181
|
+
message: pgError.message,
|
|
182
|
+
detail: pgError.detail,
|
|
183
|
+
hint: pgError.hint,
|
|
184
|
+
column: pgError.column,
|
|
185
|
+
table: pgError.table,
|
|
186
|
+
constraint: pgError.constraint,
|
|
187
|
+
dataType: pgError.dataType,
|
|
188
|
+
// Also log the outer Drizzle wrapper message for full context
|
|
189
|
+
drizzleMessage: error instanceof Error ? error.message : String(error)
|
|
190
|
+
});
|
|
191
|
+
return pgErrorToFriendlyMessage(pgError, context);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// No PG error found — log the raw error as-is
|
|
195
|
+
logger.error(`Database error in "${context}" (no PG error extracted)`, {
|
|
196
|
+
error: error instanceof Error ? error.message : String(error),
|
|
197
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
198
|
+
cause: error instanceof Error && error.cause
|
|
199
|
+
? (error.cause instanceof Error ? error.cause.message : String(error.cause))
|
|
200
|
+
: undefined
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Try to get the deepest cause message
|
|
204
|
+
const causeMessage = extractCauseMessage(error);
|
|
205
|
+
if (causeMessage) {
|
|
206
|
+
return { message: `Database error in "${context}": ${causeMessage}` };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Last resort — generic message, never leak raw SQL
|
|
210
|
+
return { message: `Could not load data for "${context}". Check server logs for details.` };
|
|
211
|
+
}
|
package/src/websocket.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { RealtimeService } from "./services/realtimeService";
|
|
2
2
|
import { PostgresBackendDriver } from "./PostgresBackendDriver";
|
|
3
|
-
import { DataDriver, DeleteEntityProps, FetchCollectionProps, FetchEntityProps, SaveEntityProps, TableMetadata, BranchInfo,
|
|
3
|
+
import type { DataDriver, DeleteEntityProps, FetchCollectionProps, FetchEntityProps, SaveEntityProps, TableMetadata, BranchInfo, AuthAdapter } from "@rebasepro/types";
|
|
4
|
+
import { isSQLAdmin, isSchemaAdmin } from "@rebasepro/types";
|
|
5
|
+
import type { User } from "@rebasepro/types";
|
|
4
6
|
import { WebSocketServer, WebSocket } from "ws";
|
|
5
7
|
import { Server } from "http";
|
|
6
8
|
import { inspect } from "util";
|
|
7
|
-
// @ts-ignore
|
|
8
9
|
import { extractUserFromToken, AccessTokenPayload } from "@rebasepro/server-core";
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
import { logger } from "@rebasepro/server-core";
|
|
11
|
+
|
|
12
|
+
/** Minimal subset of RebaseAuthConfig used by the WebSocket layer. */
|
|
13
|
+
interface WsAuthConfig {
|
|
14
|
+
requireAuth?: boolean;
|
|
15
|
+
jwtSecret?: string;
|
|
16
|
+
}
|
|
11
17
|
|
|
12
18
|
/**
|
|
13
19
|
* Normalized user identity for WebSocket sessions.
|
|
@@ -27,7 +33,6 @@ interface ClientSession {
|
|
|
27
33
|
messageWindowStart: number;
|
|
28
34
|
}
|
|
29
35
|
|
|
30
|
-
const clientSessions = new Map<string, ClientSession>();
|
|
31
36
|
|
|
32
37
|
/** Maximum messages per client per window */
|
|
33
38
|
const WS_RATE_LIMIT = 2000;
|
|
@@ -52,14 +57,14 @@ const ADMIN_ONLY_TYPES = new Set([
|
|
|
52
57
|
*/
|
|
53
58
|
function extractErrorMessage(error: unknown): string {
|
|
54
59
|
if (!error) return "Unknown error";
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return extractErrorMessage(err.cause);
|
|
59
|
-
}
|
|
60
|
-
if (typeof err.message === "string") {
|
|
61
|
-
return err.message;
|
|
60
|
+
if (error instanceof Error) {
|
|
61
|
+
if ("cause" in error && error.cause) {
|
|
62
|
+
return extractErrorMessage(error.cause);
|
|
62
63
|
}
|
|
64
|
+
return error.message;
|
|
65
|
+
}
|
|
66
|
+
if (typeof error === "object" && "message" in error && typeof (error as { message: unknown }).message === "string") {
|
|
67
|
+
return (error as { message: string }).message;
|
|
63
68
|
}
|
|
64
69
|
return String(error);
|
|
65
70
|
}
|
|
@@ -72,21 +77,20 @@ function isAdminSession(session: ClientSession | undefined): boolean {
|
|
|
72
77
|
// Fast path: new adapter-aware sessions set isAdmin directly
|
|
73
78
|
if (session.user.isAdmin) return true;
|
|
74
79
|
if (!session.user.roles) return false;
|
|
75
|
-
return session.user.roles.some((r
|
|
76
|
-
if (typeof r === "string") return r === "admin";
|
|
77
|
-
if (r && typeof r === "object" && "isAdmin" in r) return (r as { isAdmin: boolean }).isAdmin;
|
|
78
|
-
if (r && typeof r === "object" && "id" in r) return (r as { id: string }).id === "admin";
|
|
79
|
-
return false;
|
|
80
|
-
});
|
|
80
|
+
return session.user.roles.some((r) => r === "admin");
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
export function createPostgresWebSocket(
|
|
84
84
|
server: Server,
|
|
85
85
|
realtimeService: RealtimeService,
|
|
86
86
|
driver: PostgresBackendDriver,
|
|
87
|
-
authConfig?:
|
|
87
|
+
authConfig?: WsAuthConfig,
|
|
88
88
|
authAdapter?: AuthAdapter
|
|
89
89
|
) {
|
|
90
|
+
// Session map scoped to this factory invocation — prevents stale sessions
|
|
91
|
+
// leaking across hot reloads or multiple factory calls.
|
|
92
|
+
const clientSessions = new Map<string, ClientSession>();
|
|
93
|
+
|
|
90
94
|
const isProduction = process.env.NODE_ENV === "production";
|
|
91
95
|
/** Debug logger that is suppressed in production to prevent PII/data leaks */
|
|
92
96
|
const wsDebug = (...args: unknown[]) => { if (!isProduction) console.debug(...args); };
|
|
@@ -101,7 +105,7 @@ export function createPostgresWebSocket(
|
|
|
101
105
|
// Silently absorbed — listenWithPortRetry will retry the next port
|
|
102
106
|
return;
|
|
103
107
|
}
|
|
104
|
-
|
|
108
|
+
logger.error("❌ [WebSocket Server] Error", { error: err });
|
|
105
109
|
});
|
|
106
110
|
|
|
107
111
|
// Auth is required when either: an adapter is present (secure by default),
|
|
@@ -166,14 +170,14 @@ code } }
|
|
|
166
170
|
const adapterUser = authAdapter.verifyToken
|
|
167
171
|
? await authAdapter.verifyToken(token)
|
|
168
172
|
: await authAdapter.verifyRequest(new Request("http://localhost/_ws_auth", {
|
|
169
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
173
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
170
174
|
}));
|
|
171
175
|
|
|
172
176
|
if (adapterUser) {
|
|
173
177
|
verifiedUser = {
|
|
174
178
|
userId: adapterUser.uid,
|
|
175
179
|
roles: adapterUser.roles,
|
|
176
|
-
isAdmin: adapterUser.isAdmin
|
|
180
|
+
isAdmin: adapterUser.isAdmin
|
|
177
181
|
};
|
|
178
182
|
}
|
|
179
183
|
} catch {
|
|
@@ -186,7 +190,7 @@ code } }
|
|
|
186
190
|
verifiedUser = {
|
|
187
191
|
userId: jwtPayload.userId,
|
|
188
192
|
roles: jwtPayload.roles ?? [],
|
|
189
|
-
isAdmin: (jwtPayload.roles ?? []).some((r: string) => r === "admin")
|
|
193
|
+
isAdmin: (jwtPayload.roles ?? []).some((r: string) => r === "admin")
|
|
190
194
|
};
|
|
191
195
|
}
|
|
192
196
|
}
|
|
@@ -201,7 +205,8 @@ code } }
|
|
|
201
205
|
ws.send(JSON.stringify({
|
|
202
206
|
type: "AUTH_SUCCESS",
|
|
203
207
|
requestId,
|
|
204
|
-
payload: { userId: verifiedUser.userId,
|
|
208
|
+
payload: { userId: verifiedUser.userId,
|
|
209
|
+
roles: verifiedUser.roles }
|
|
205
210
|
}));
|
|
206
211
|
wsDebug(`🔐 [WebSocket Server] Client ${clientId} authenticated as ${verifiedUser.userId}`);
|
|
207
212
|
} else {
|
|
@@ -249,20 +254,31 @@ code } }
|
|
|
249
254
|
// Helper to get correctly scoped delegate for the current request
|
|
250
255
|
const getScopedDelegate = async (): Promise<DataDriver> => {
|
|
251
256
|
const session = clientSessions.get(clientId);
|
|
252
|
-
|
|
257
|
+
// Check if the driver supports RLS-scoped delegates
|
|
258
|
+
if (typeof driver.withAuth === "function") {
|
|
253
259
|
try {
|
|
254
|
-
const userForAuth = session?.user
|
|
260
|
+
const userForAuth: User = session?.user
|
|
255
261
|
? {
|
|
256
262
|
uid: session.user.userId,
|
|
263
|
+
displayName: null,
|
|
264
|
+
email: null,
|
|
265
|
+
photoURL: null,
|
|
266
|
+
providerId: "websocket",
|
|
267
|
+
isAnonymous: false,
|
|
257
268
|
roles: session.user.roles ?? []
|
|
258
269
|
}
|
|
259
270
|
: {
|
|
260
271
|
uid: "anon",
|
|
272
|
+
displayName: null,
|
|
273
|
+
email: null,
|
|
274
|
+
photoURL: null,
|
|
275
|
+
providerId: "websocket",
|
|
276
|
+
isAnonymous: true,
|
|
261
277
|
roles: ["anon"]
|
|
262
278
|
};
|
|
263
|
-
return await
|
|
279
|
+
return await driver.withAuth(userForAuth);
|
|
264
280
|
} catch (e) {
|
|
265
|
-
|
|
281
|
+
logger.error("Failed to create RLS scoped delegate for WS request", { error: e });
|
|
266
282
|
throw new Error("Internal authentication error");
|
|
267
283
|
}
|
|
268
284
|
}
|
|
@@ -561,8 +577,10 @@ colors: true }));
|
|
|
561
577
|
// Attach auth context from the WS session so RLS-aware refetches work
|
|
562
578
|
const session = clientSessions.get(clientId);
|
|
563
579
|
const authContext = session?.user
|
|
564
|
-
? { userId: session.user.userId,
|
|
565
|
-
|
|
580
|
+
? { userId: session.user.userId,
|
|
581
|
+
roles: session.user.roles ?? [] }
|
|
582
|
+
: { userId: "anon",
|
|
583
|
+
roles: ["anon"] };
|
|
566
584
|
// Let RealtimeService handle these messages
|
|
567
585
|
await realtimeService.handleClientMessage(clientId, {
|
|
568
586
|
type,
|
|
@@ -573,12 +591,12 @@ colors: true }));
|
|
|
573
591
|
}
|
|
574
592
|
|
|
575
593
|
default:
|
|
576
|
-
|
|
594
|
+
logger.error("❌ [WebSocket Server] Unknown message type", { detail: type });
|
|
577
595
|
}
|
|
578
596
|
} catch (error: unknown) {
|
|
579
|
-
|
|
597
|
+
logger.error("💥 [WebSocket Server] Error handling message", { error: error });
|
|
580
598
|
if (error instanceof Error) {
|
|
581
|
-
|
|
599
|
+
logger.error("Stack trace", { detail: error.stack });
|
|
582
600
|
}
|
|
583
601
|
const errorMessage = process.env.NODE_ENV === "production"
|
|
584
602
|
? "An unexpected error occurred"
|
|
@@ -8,7 +8,9 @@ jest.mock("drizzle-orm", () => {
|
|
|
8
8
|
const actual = jest.requireActual("drizzle-orm");
|
|
9
9
|
return {
|
|
10
10
|
...actual,
|
|
11
|
-
eq: jest.fn((field, value) => ({ field,
|
|
11
|
+
eq: jest.fn((field, value) => ({ field,
|
|
12
|
+
value,
|
|
13
|
+
type: "eq" })),
|
|
12
14
|
sql: Object.assign(
|
|
13
15
|
jest.fn((strings: TemplateStringsArray, ...values: unknown[]) => ({
|
|
14
16
|
strings,
|
|
@@ -16,8 +18,11 @@ jest.mock("drizzle-orm", () => {
|
|
|
16
18
|
type: "sql"
|
|
17
19
|
})),
|
|
18
20
|
{
|
|
19
|
-
raw: jest.fn((val: string) => ({ val,
|
|
20
|
-
|
|
21
|
+
raw: jest.fn((val: string) => ({ val,
|
|
22
|
+
type: "sql-raw" })),
|
|
23
|
+
join: jest.fn((parts: unknown[], separator: unknown) => ({ parts,
|
|
24
|
+
separator,
|
|
25
|
+
type: "sql-join" }))
|
|
21
26
|
}
|
|
22
27
|
),
|
|
23
28
|
relations: jest.fn(() => ({}))
|
|
@@ -133,11 +138,11 @@ describe("Auth Services", () => {
|
|
|
133
138
|
email: "test@example.com",
|
|
134
139
|
displayName: "Test User"
|
|
135
140
|
};
|
|
136
|
-
const dbReturnedUser = {
|
|
141
|
+
const dbReturnedUser = {
|
|
137
142
|
id: "user-123",
|
|
138
143
|
...newUser,
|
|
139
144
|
createdAt: new Date(),
|
|
140
|
-
updatedAt: new Date()
|
|
145
|
+
updatedAt: new Date()
|
|
141
146
|
};
|
|
142
147
|
mockInsertReturning.mockResolvedValueOnce([dbReturnedUser]);
|
|
143
148
|
|
|
@@ -154,7 +159,8 @@ describe("Auth Services", () => {
|
|
|
154
159
|
|
|
155
160
|
describe("getUserById", () => {
|
|
156
161
|
it("should return user when found", async () => {
|
|
157
|
-
const mockUser = { id: "user-123",
|
|
162
|
+
const mockUser = { id: "user-123",
|
|
163
|
+
email: "test@example.com" };
|
|
158
164
|
mockSelectWhere.mockResolvedValueOnce([mockUser]);
|
|
159
165
|
|
|
160
166
|
const result = await userService.getUserById("user-123");
|
|
@@ -174,7 +180,8 @@ describe("Auth Services", () => {
|
|
|
174
180
|
|
|
175
181
|
describe("getUserByEmail", () => {
|
|
176
182
|
it("should return user when found by email", async () => {
|
|
177
|
-
const mockUser = { id: "user-123",
|
|
183
|
+
const mockUser = { id: "user-123",
|
|
184
|
+
email: "test@example.com" };
|
|
178
185
|
mockSelectWhere.mockResolvedValueOnce([mockUser]);
|
|
179
186
|
|
|
180
187
|
const result = await userService.getUserByEmail("test@example.com");
|
|
@@ -194,13 +201,15 @@ describe("Auth Services", () => {
|
|
|
194
201
|
|
|
195
202
|
describe("getUserByIdentity", () => {
|
|
196
203
|
it("should fetch user by identity", async () => {
|
|
197
|
-
const mockUser = { id: "user-123",
|
|
204
|
+
const mockUser = { id: "user-123",
|
|
205
|
+
email: "test@example.com" };
|
|
198
206
|
mockSelectWhere.mockResolvedValueOnce([{ user: mockUser }]);
|
|
199
207
|
|
|
200
208
|
const result = await userService.getUserByIdentity("google", "google-abc");
|
|
201
209
|
|
|
202
210
|
expect(db.select).toHaveBeenCalled();
|
|
203
|
-
expect(result).toEqual(expect.objectContaining({ id: "user-123",
|
|
211
|
+
expect(result).toEqual(expect.objectContaining({ id: "user-123",
|
|
212
|
+
email: "test@example.com" }));
|
|
204
213
|
});
|
|
205
214
|
});
|
|
206
215
|
|
|
@@ -223,10 +232,10 @@ describe("Auth Services", () => {
|
|
|
223
232
|
|
|
224
233
|
describe("updateUser", () => {
|
|
225
234
|
it("should update user and return updated record", async () => {
|
|
226
|
-
const updatedUser = {
|
|
235
|
+
const updatedUser = {
|
|
227
236
|
id: "user-123",
|
|
228
237
|
email: "test@example.com",
|
|
229
|
-
displayName: "Updated Name"
|
|
238
|
+
displayName: "Updated Name"
|
|
230
239
|
};
|
|
231
240
|
mockUpdateReturning.mockResolvedValueOnce([updatedUser]);
|
|
232
241
|
|
|
@@ -261,8 +270,10 @@ describe("Auth Services", () => {
|
|
|
261
270
|
describe("listUsers", () => {
|
|
262
271
|
it("should return all users", async () => {
|
|
263
272
|
const mockUsers = [
|
|
264
|
-
{ id: "user-1",
|
|
265
|
-
|
|
273
|
+
{ id: "user-1",
|
|
274
|
+
email: "user1@example.com" },
|
|
275
|
+
{ id: "user-2",
|
|
276
|
+
email: "user2@example.com" }
|
|
266
277
|
];
|
|
267
278
|
mockSelectFrom.mockReturnValueOnce(Promise.resolve(mockUsers));
|
|
268
279
|
|
|
@@ -270,8 +281,10 @@ describe("Auth Services", () => {
|
|
|
270
281
|
|
|
271
282
|
expect(db.select).toHaveBeenCalled();
|
|
272
283
|
expect(result).toEqual([
|
|
273
|
-
mockUserData({ id: "user-1",
|
|
274
|
-
|
|
284
|
+
mockUserData({ id: "user-1",
|
|
285
|
+
email: "user1@example.com" }),
|
|
286
|
+
mockUserData({ id: "user-2",
|
|
287
|
+
email: "user2@example.com" })
|
|
275
288
|
]);
|
|
276
289
|
});
|
|
277
290
|
});
|
|
@@ -332,7 +345,8 @@ describe("Auth Services", () => {
|
|
|
332
345
|
|
|
333
346
|
describe("getUserByVerificationToken", () => {
|
|
334
347
|
it("should find user by verification token", async () => {
|
|
335
|
-
const mockUser = { id: "user-123",
|
|
348
|
+
const mockUser = { id: "user-123",
|
|
349
|
+
email: "test@example.com" };
|
|
336
350
|
mockSelectWhere.mockResolvedValueOnce([mockUser]);
|
|
337
351
|
|
|
338
352
|
const result = await userService.getUserByVerificationToken("token-abc");
|
|
@@ -396,7 +410,8 @@ describe("Auth Services", () => {
|
|
|
396
410
|
|
|
397
411
|
describe("getUserWithRoles", () => {
|
|
398
412
|
it("should return user with roles", async () => {
|
|
399
|
-
const mockUser = { id: "user-123",
|
|
413
|
+
const mockUser = { id: "user-123",
|
|
414
|
+
email: "test@example.com" };
|
|
400
415
|
mockSelectWhere.mockResolvedValueOnce([mockUser]);
|
|
401
416
|
mockExecute.mockResolvedValueOnce({
|
|
402
417
|
rows: [{ roles: ["admin"] }]
|
|
@@ -427,7 +442,8 @@ describe("Auth Services", () => {
|
|
|
427
442
|
it("should return paginated and filtered users list", async () => {
|
|
428
443
|
mockExecute
|
|
429
444
|
.mockResolvedValueOnce({ rows: [{ total: 1 }] })
|
|
430
|
-
.mockResolvedValueOnce({ rows: [{ id: "user-123",
|
|
445
|
+
.mockResolvedValueOnce({ rows: [{ id: "user-123",
|
|
446
|
+
email: "test@example.com" }] });
|
|
431
447
|
|
|
432
448
|
const result = await userService.listUsersPaginated({
|
|
433
449
|
limit: 10,
|
|
@@ -439,7 +455,8 @@ describe("Auth Services", () => {
|
|
|
439
455
|
|
|
440
456
|
expect(mockExecute).toHaveBeenCalledTimes(2);
|
|
441
457
|
expect(result).toEqual({
|
|
442
|
-
users: [mockUserData({ id: "user-123",
|
|
458
|
+
users: [mockUserData({ id: "user-123",
|
|
459
|
+
email: "test@example.com" })],
|
|
443
460
|
total: 1,
|
|
444
461
|
limit: 10,
|
|
445
462
|
offset: 0
|