@rebasepro/server-postgresql 0.3.0 → 0.4.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/dist/common/src/collections/default-collections.d.ts +5 -8
- package/dist/common/src/data/query_builder.d.ts +6 -2
- package/dist/index.es.js +301 -500
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +297 -496
- package/dist/index.umd.js.map +1 -1
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +2 -0
- package/dist/server-postgresql/src/auth/ensure-tables.d.ts +7 -4
- package/dist/server-postgresql/src/auth/services.d.ts +6 -31
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +87 -340
- package/dist/server-postgresql/src/services/EntityFetchService.d.ts +2 -1
- package/dist/server-postgresql/src/services/EntityPersistService.d.ts +4 -0
- package/dist/server-postgresql/src/services/entityService.d.ts +4 -0
- package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +5 -1
- package/dist/types/src/controllers/auth.d.ts +2 -2
- package/dist/types/src/controllers/client.d.ts +25 -40
- package/dist/types/src/controllers/data.d.ts +21 -3
- package/dist/types/src/controllers/data_driver.d.ts +5 -0
- package/dist/types/src/controllers/email.d.ts +2 -0
- package/dist/types/src/types/auth_adapter.d.ts +3 -56
- package/dist/types/src/types/backend.d.ts +2 -2
- package/dist/types/src/types/backend_hooks.d.ts +2 -17
- package/dist/types/src/types/collections.d.ts +9 -5
- package/dist/types/src/types/entity_views.d.ts +19 -28
- package/dist/types/src/types/properties.d.ts +9 -7
- package/dist/types/src/types/user_management_delegate.d.ts +16 -53
- package/dist/types/src/users/index.d.ts +0 -1
- package/dist/types/src/users/user.d.ts +0 -1
- package/package.json +6 -6
- package/src/PostgresBackendDriver.ts +10 -0
- package/src/PostgresBootstrapper.ts +25 -21
- package/src/auth/ensure-tables.ts +82 -129
- package/src/auth/services.ts +71 -170
- package/src/schema/auth-schema.ts +13 -69
- package/src/schema/doctor.ts +44 -3
- package/src/schema/generate-drizzle-schema-logic.ts +33 -3
- package/src/schema/generate-drizzle-schema.ts +2 -6
- package/src/schema/introspect-db-logic.ts +7 -0
- package/src/services/EntityFetchService.ts +13 -1
- package/src/services/EntityPersistService.ts +9 -0
- package/src/services/entityService.ts +7 -0
- package/src/utils/drizzle-conditions.ts +40 -5
- package/src/websocket.ts +1 -3
- package/test/auth-services.test.ts +7 -150
- package/test/doctor.test.ts +6 -2
- package/test/relation-pipeline-gaps.test.ts +315 -0
- package/dist/server-postgresql/src/schema/default-collections.d.ts +0 -2
- package/dist/types/src/users/roles.d.ts +0 -14
- package/src/schema/default-collections.ts +0 -69
|
@@ -70,6 +70,8 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCo
|
|
|
70
70
|
columnDefinition = `${enumName}("${colName}")`;
|
|
71
71
|
} else if ("isId" in stringProp && stringProp.isId === "uuid") {
|
|
72
72
|
columnDefinition = `uuid("${colName}")`;
|
|
73
|
+
} else if (stringProp.columnType === "uuid") {
|
|
74
|
+
columnDefinition = `uuid("${colName}")`;
|
|
73
75
|
} else if (stringProp.columnType === "text") {
|
|
74
76
|
columnDefinition = `text("${colName}")`;
|
|
75
77
|
} else if (stringProp.columnType === "char") {
|
|
@@ -145,11 +147,39 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: EntityCo
|
|
|
145
147
|
}
|
|
146
148
|
break;
|
|
147
149
|
}
|
|
148
|
-
case "map":
|
|
150
|
+
case "map": {
|
|
151
|
+
const mapProp = prop as MapProperty;
|
|
152
|
+
if (mapProp.columnType === "json") {
|
|
153
|
+
columnDefinition = `json("${colName}")`;
|
|
154
|
+
} else {
|
|
155
|
+
columnDefinition = `jsonb("${colName}")`;
|
|
156
|
+
}
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
149
159
|
case "array": {
|
|
150
|
-
const
|
|
151
|
-
|
|
160
|
+
const arrayProp = prop as ArrayProperty;
|
|
161
|
+
let colType = arrayProp.columnType;
|
|
162
|
+
if (!colType && arrayProp.of && !Array.isArray(arrayProp.of)) {
|
|
163
|
+
const ofProp = arrayProp.of as Property;
|
|
164
|
+
if (ofProp.type === "string") {
|
|
165
|
+
colType = "text[]";
|
|
166
|
+
} else if (ofProp.type === "number") {
|
|
167
|
+
colType = ofProp.validation?.integer ? "integer[]" : "numeric[]";
|
|
168
|
+
} else if (ofProp.type === "boolean") {
|
|
169
|
+
colType = "boolean[]";
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (colType === "json") {
|
|
152
174
|
columnDefinition = `json("${colName}")`;
|
|
175
|
+
} else if (colType === "text[]") {
|
|
176
|
+
columnDefinition = `text("${colName}").array()`;
|
|
177
|
+
} else if (colType === "integer[]") {
|
|
178
|
+
columnDefinition = `integer("${colName}").array()`;
|
|
179
|
+
} else if (colType === "boolean[]") {
|
|
180
|
+
columnDefinition = `boolean("${colName}").array()`;
|
|
181
|
+
} else if (colType === "numeric[]") {
|
|
182
|
+
columnDefinition = `numeric("${colName}").array()`;
|
|
153
183
|
} else {
|
|
154
184
|
columnDefinition = `jsonb("${colName}")`;
|
|
155
185
|
}
|
|
@@ -5,7 +5,7 @@ import { pathToFileURL } from "url";
|
|
|
5
5
|
import chokidar from "chokidar";
|
|
6
6
|
import { generateSchema } from "./generate-drizzle-schema-logic";
|
|
7
7
|
import { EntityCollection } from "@rebasepro/types";
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
|
|
10
10
|
// --- Helper Functions ---
|
|
11
11
|
|
|
@@ -90,11 +90,7 @@ const runGeneration = async (collectionsFilePath?: string, outputPath?: string)
|
|
|
90
90
|
collections = [];
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
// Map keyed by slug — last-write-wins, so developer collections overwrite defaults
|
|
95
|
-
collections = Array.from(
|
|
96
|
-
new Map([defaultUsersCollection, ...collections].map(c => [c.slug, c])).values()
|
|
97
|
-
);
|
|
93
|
+
|
|
98
94
|
|
|
99
95
|
// Sort collections by slug alphabetically to ensure deterministic schema generation
|
|
100
96
|
collections.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
@@ -589,9 +589,16 @@ export function generateCollectionFile(
|
|
|
589
589
|
// Array/Map heuristics (Fallback if not inferred)
|
|
590
590
|
if (finalPropType === "array" && !inferenceExtra.includes("of: {")) {
|
|
591
591
|
let innerType = "string";
|
|
592
|
+
let colType = "";
|
|
592
593
|
if (col.udt_name.startsWith("_")) {
|
|
593
594
|
const baseType = col.udt_name.substring(1);
|
|
594
595
|
innerType = mapPgType(baseType);
|
|
596
|
+
if (innerType === "string") colType = "text[]";
|
|
597
|
+
else if (innerType === "number") colType = col.udt_name === "_numeric" ? "numeric[]" : "integer[]";
|
|
598
|
+
else if (innerType === "boolean") colType = "boolean[]";
|
|
599
|
+
}
|
|
600
|
+
if (colType) {
|
|
601
|
+
extra += `\n columnType: "${colType}",`;
|
|
595
602
|
}
|
|
596
603
|
extra += `\n of: { name: "${humanize(col.column_name)} Item", type: "${innerType}" },`;
|
|
597
604
|
} else if (finalPropType === "map" && !inferenceExtra.includes("keyValue: true") && !inferenceExtra.includes("properties: {")) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { and, asc, count, desc, eq, getTableName, gt, lt, or, SQL, TableRelationalConfig, TablesRelationalConfig } from "drizzle-orm";
|
|
2
2
|
import { AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
|
|
3
|
-
import { Entity, EntityCollection, FilterValues, Relation } from "@rebasepro/types";
|
|
3
|
+
import { Entity, EntityCollection, FilterValues, Relation, LogicalCondition } from "@rebasepro/types";
|
|
4
4
|
import type { VectorSearchParams } from "@rebasepro/types";
|
|
5
5
|
import { resolveCollectionRelations, findRelation, createRelationRef, createRelationRefWithData } from "@rebasepro/common";
|
|
6
6
|
import { DrizzleConditionBuilder } from "../utils/drizzle-conditions";
|
|
@@ -465,6 +465,7 @@ export class EntityFetchService {
|
|
|
465
465
|
offset?: number;
|
|
466
466
|
startAfter?: Record<string, unknown>;
|
|
467
467
|
searchString?: string;
|
|
468
|
+
logical?: LogicalCondition;
|
|
468
469
|
},
|
|
469
470
|
collectionPath: string,
|
|
470
471
|
withConfig?: Record<string, unknown>
|
|
@@ -494,6 +495,11 @@ export class EntityFetchService {
|
|
|
494
495
|
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
495
496
|
}
|
|
496
497
|
|
|
498
|
+
if (options.logical) {
|
|
499
|
+
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath);
|
|
500
|
+
if (logicalCondition) allConditions.push(logicalCondition);
|
|
501
|
+
}
|
|
502
|
+
|
|
497
503
|
// Cursor-based pagination (startAfter)
|
|
498
504
|
if (options.startAfter) {
|
|
499
505
|
const cursorConditions = this.buildCursorConditions(table, idField, idInfo, options, collectionPath);
|
|
@@ -700,6 +706,7 @@ export class EntityFetchService {
|
|
|
700
706
|
searchString?: string;
|
|
701
707
|
databaseId?: string;
|
|
702
708
|
vectorSearch?: VectorSearchParams;
|
|
709
|
+
logical?: LogicalCondition;
|
|
703
710
|
} = {}
|
|
704
711
|
): Promise<Entity<M>[]> {
|
|
705
712
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
@@ -774,6 +781,11 @@ export class EntityFetchService {
|
|
|
774
781
|
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
775
782
|
}
|
|
776
783
|
|
|
784
|
+
if (options.logical) {
|
|
785
|
+
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath);
|
|
786
|
+
if (logicalCondition) allConditions.push(logicalCondition);
|
|
787
|
+
}
|
|
788
|
+
|
|
777
789
|
// Vector distance threshold filter
|
|
778
790
|
if (vectorMeta?.filter) {
|
|
779
791
|
allConditions.push(vectorMeta.filter);
|
|
@@ -53,6 +53,15 @@ export class EntityPersistService {
|
|
|
53
53
|
.where(eq(idField, parsedId));
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Delete all entities from a collection
|
|
58
|
+
*/
|
|
59
|
+
async deleteAll(collectionPath: string, _databaseId?: string): Promise<void> {
|
|
60
|
+
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
61
|
+
const table = getTableForCollection(collection, this.registry);
|
|
62
|
+
await this.db.delete(table);
|
|
63
|
+
}
|
|
64
|
+
|
|
56
65
|
/**
|
|
57
66
|
* Save an entity (create or update)
|
|
58
67
|
*/
|
|
@@ -169,6 +169,13 @@ export class EntityService implements EntityRepository {
|
|
|
169
169
|
return this.persistService.deleteEntity(collectionPath, entityId, databaseId);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Delete all entities from a collection
|
|
174
|
+
*/
|
|
175
|
+
async deleteAll(collectionPath: string, databaseId?: string): Promise<void> {
|
|
176
|
+
return this.persistService.deleteAll(collectionPath, databaseId);
|
|
177
|
+
}
|
|
178
|
+
|
|
172
179
|
|
|
173
180
|
/**
|
|
174
181
|
* Execute raw SQL
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { and, eq, or, sql, SQL, ilike, inArray } from "drizzle-orm";
|
|
2
2
|
import { AnyPgColumn, PgTable, PgVarchar, PgText, PgChar } from "drizzle-orm/pg-core";
|
|
3
|
-
import { FilterValues, WhereFilterOp, Relation, JoinStep } from "@rebasepro/types";
|
|
3
|
+
import { FilterValues, WhereFilterOp, Relation, JoinStep, LogicalCondition, FilterCondition } from "@rebasepro/types";
|
|
4
4
|
import { getColumnName, resolveCollectionRelations } from "@rebasepro/common";
|
|
5
5
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
6
6
|
import { ConditionBuilderStatic } from "../interfaces";
|
|
@@ -37,7 +37,6 @@ export class DrizzleConditionBuilder {
|
|
|
37
37
|
for (const [field, filterParam] of Object.entries(filter)) {
|
|
38
38
|
if (!filterParam) continue;
|
|
39
39
|
|
|
40
|
-
const [op, value] = filterParam as [WhereFilterOp, any];
|
|
41
40
|
let fieldColumn = table[field as keyof typeof table] as AnyPgColumn;
|
|
42
41
|
|
|
43
42
|
if (!fieldColumn) {
|
|
@@ -53,15 +52,51 @@ export class DrizzleConditionBuilder {
|
|
|
53
52
|
continue;
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
const paramsList = Array.isArray(filterParam) && filterParam.length > 0 && Array.isArray(filterParam[0])
|
|
56
|
+
? (filterParam as [WhereFilterOp, any][])
|
|
57
|
+
: [filterParam as [WhereFilterOp, any]];
|
|
58
|
+
|
|
59
|
+
for (const [op, value] of paramsList) {
|
|
60
|
+
const condition = this.buildSingleFilterCondition(fieldColumn, op, value);
|
|
61
|
+
if (condition) {
|
|
62
|
+
conditions.push(condition);
|
|
63
|
+
}
|
|
59
64
|
}
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
return conditions;
|
|
63
68
|
}
|
|
64
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Build logical conditions recursively from LogicalCondition or FilterCondition
|
|
72
|
+
*/
|
|
73
|
+
static buildLogicalConditions(
|
|
74
|
+
cond: LogicalCondition | FilterCondition,
|
|
75
|
+
table: PgTable<any>,
|
|
76
|
+
collectionPath: string
|
|
77
|
+
): SQL | null {
|
|
78
|
+
if ("type" in cond) {
|
|
79
|
+
const subSQLs = cond.conditions
|
|
80
|
+
.map(c => this.buildLogicalConditions(c, table, collectionPath))
|
|
81
|
+
.filter((sql): sql is SQL => sql !== null);
|
|
82
|
+
if (subSQLs.length === 0) return null;
|
|
83
|
+
return (cond.type === "or" ? or(...subSQLs) : and(...subSQLs)) ?? null;
|
|
84
|
+
} else {
|
|
85
|
+
let fieldColumn = table[cond.column as keyof typeof table] as AnyPgColumn;
|
|
86
|
+
if (!fieldColumn) {
|
|
87
|
+
const relationKey = `${cond.column}_id`;
|
|
88
|
+
if (relationKey in table) {
|
|
89
|
+
fieldColumn = table[relationKey as keyof typeof table] as AnyPgColumn;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!fieldColumn) {
|
|
93
|
+
console.warn(`Filtering by field '${cond.column}', but it does not exist in table for collection '${collectionPath}'`);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return this.buildSingleFilterCondition(fieldColumn, cond.operator as WhereFilterOp, cond.value);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
65
100
|
/**
|
|
66
101
|
* Build a single filter condition for a specific operator and value
|
|
67
102
|
*/
|
package/src/websocket.ts
CHANGED
|
@@ -547,15 +547,13 @@ colors: true }));
|
|
|
547
547
|
}
|
|
548
548
|
break;
|
|
549
549
|
|
|
550
|
-
// Route subscription messages to RealtimeService
|
|
550
|
+
// Route subscription messages, broadcast channels, and presence to RealtimeService
|
|
551
551
|
case "subscribe_collection":
|
|
552
552
|
case "subscribe_entity":
|
|
553
553
|
case "unsubscribe":
|
|
554
|
-
// Broadcast channels
|
|
555
554
|
case "join_channel":
|
|
556
555
|
case "leave_channel":
|
|
557
556
|
case "broadcast":
|
|
558
|
-
// Presence
|
|
559
557
|
case "presence_track":
|
|
560
558
|
case "presence_untrack":
|
|
561
559
|
case "presence_state": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
|
-
import { UserService,
|
|
2
|
+
import { UserService, RefreshTokenService, PasswordResetTokenService, Role } from "../src/auth/services";
|
|
3
3
|
import { users, refreshTokens, passwordResetTokens } from "../src/schema/auth-schema";
|
|
4
4
|
import { UserData } from "@rebasepro/server-core";
|
|
5
5
|
|
|
@@ -344,18 +344,7 @@ describe("Auth Services", () => {
|
|
|
344
344
|
describe("getUserRoles", () => {
|
|
345
345
|
it("should return roles for user", async () => {
|
|
346
346
|
mockExecute.mockResolvedValueOnce({
|
|
347
|
-
rows: [
|
|
348
|
-
{ id: "admin",
|
|
349
|
-
name: "Admin",
|
|
350
|
-
is_admin: true,
|
|
351
|
-
default_permissions: null,
|
|
352
|
-
collection_permissions: null },
|
|
353
|
-
{ id: "editor",
|
|
354
|
-
name: "Editor",
|
|
355
|
-
is_admin: false,
|
|
356
|
-
default_permissions: { edit: true },
|
|
357
|
-
collection_permissions: null }
|
|
358
|
-
]
|
|
347
|
+
rows: [{ roles: ["admin", "editor"] }]
|
|
359
348
|
});
|
|
360
349
|
|
|
361
350
|
const roles = await userService.getUserRoles("user-123");
|
|
@@ -363,7 +352,7 @@ describe("Auth Services", () => {
|
|
|
363
352
|
expect(roles).toHaveLength(2);
|
|
364
353
|
expect(roles[0]).toEqual({
|
|
365
354
|
id: "admin",
|
|
366
|
-
name: "
|
|
355
|
+
name: "admin",
|
|
367
356
|
isAdmin: true,
|
|
368
357
|
defaultPermissions: null,
|
|
369
358
|
collectionPermissions: null
|
|
@@ -374,13 +363,7 @@ describe("Auth Services", () => {
|
|
|
374
363
|
describe("getUserRoleIds", () => {
|
|
375
364
|
it("should return role IDs for user", async () => {
|
|
376
365
|
mockExecute.mockResolvedValueOnce({
|
|
377
|
-
rows: [
|
|
378
|
-
{ id: "admin",
|
|
379
|
-
name: "Admin",
|
|
380
|
-
is_admin: true,
|
|
381
|
-
default_permissions: null,
|
|
382
|
-
collection_permissions: null }
|
|
383
|
-
]
|
|
366
|
+
rows: [{ roles: ["admin"] }]
|
|
384
367
|
});
|
|
385
368
|
|
|
386
369
|
const roleIds = await userService.getUserRoleIds("user-123");
|
|
@@ -393,10 +376,7 @@ describe("Auth Services", () => {
|
|
|
393
376
|
it("should delete existing and insert new roles", async () => {
|
|
394
377
|
await userService.setUserRoles("user-123", ["admin", "editor"]);
|
|
395
378
|
|
|
396
|
-
// First call deletes existing roles
|
|
397
379
|
expect(mockExecute).toHaveBeenCalled();
|
|
398
|
-
// Subsequent calls insert new roles
|
|
399
|
-
expect(mockExecute.mock.calls.length).toBeGreaterThanOrEqual(1);
|
|
400
380
|
});
|
|
401
381
|
});
|
|
402
382
|
|
|
@@ -408,7 +388,7 @@ describe("Auth Services", () => {
|
|
|
408
388
|
});
|
|
409
389
|
|
|
410
390
|
it("should use editor as default role", async () => {
|
|
411
|
-
await userService.assignDefaultRole("user-123");
|
|
391
|
+
await userService.assignDefaultRole("user-123", "editor");
|
|
412
392
|
|
|
413
393
|
expect(mockExecute).toHaveBeenCalled();
|
|
414
394
|
});
|
|
@@ -419,11 +399,7 @@ describe("Auth Services", () => {
|
|
|
419
399
|
const mockUser = { id: "user-123", email: "test@example.com" };
|
|
420
400
|
mockSelectWhere.mockResolvedValueOnce([mockUser]);
|
|
421
401
|
mockExecute.mockResolvedValueOnce({
|
|
422
|
-
rows: [{
|
|
423
|
-
name: "Admin",
|
|
424
|
-
is_admin: true,
|
|
425
|
-
default_permissions: null,
|
|
426
|
-
collection_permissions: null }]
|
|
402
|
+
rows: [{ roles: ["admin"] }]
|
|
427
403
|
});
|
|
428
404
|
|
|
429
405
|
const result = await userService.getUserWithRoles("user-123");
|
|
@@ -431,7 +407,7 @@ describe("Auth Services", () => {
|
|
|
431
407
|
expect(result).toEqual({
|
|
432
408
|
user: mockUserData({}),
|
|
433
409
|
roles: [{ id: "admin",
|
|
434
|
-
name: "
|
|
410
|
+
name: "admin",
|
|
435
411
|
isAdmin: true,
|
|
436
412
|
defaultPermissions: null,
|
|
437
413
|
collectionPermissions: null }]
|
|
@@ -472,125 +448,6 @@ describe("Auth Services", () => {
|
|
|
472
448
|
});
|
|
473
449
|
});
|
|
474
450
|
|
|
475
|
-
describe("RoleService", () => {
|
|
476
|
-
let roleService: RoleService;
|
|
477
|
-
|
|
478
|
-
beforeEach(() => {
|
|
479
|
-
roleService = new RoleService(db);
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
describe("getRoleById", () => {
|
|
483
|
-
it("should return role when found", async () => {
|
|
484
|
-
mockExecute.mockResolvedValueOnce({
|
|
485
|
-
rows: [{ id: "admin",
|
|
486
|
-
name: "Admin",
|
|
487
|
-
is_admin: true,
|
|
488
|
-
default_permissions: null,
|
|
489
|
-
collection_permissions: null }]
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
const result = await roleService.getRoleById("admin");
|
|
493
|
-
|
|
494
|
-
expect(result).toEqual({
|
|
495
|
-
id: "admin",
|
|
496
|
-
name: "Admin",
|
|
497
|
-
isAdmin: true,
|
|
498
|
-
defaultPermissions: null,
|
|
499
|
-
collectionPermissions: null
|
|
500
|
-
});
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
it("should return null when role not found", async () => {
|
|
504
|
-
mockExecute.mockResolvedValueOnce({ rows: [] });
|
|
505
|
-
|
|
506
|
-
const result = await roleService.getRoleById("nonexistent");
|
|
507
|
-
|
|
508
|
-
expect(result).toBeNull();
|
|
509
|
-
});
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
describe("listRoles", () => {
|
|
513
|
-
it("should return all roles", async () => {
|
|
514
|
-
mockExecute.mockResolvedValueOnce({
|
|
515
|
-
rows: [
|
|
516
|
-
{ id: "admin",
|
|
517
|
-
name: "Admin",
|
|
518
|
-
is_admin: true,
|
|
519
|
-
default_permissions: null,
|
|
520
|
-
collection_permissions: null },
|
|
521
|
-
{ id: "editor",
|
|
522
|
-
name: "Editor",
|
|
523
|
-
is_admin: false,
|
|
524
|
-
default_permissions: null,
|
|
525
|
-
collection_permissions: null }
|
|
526
|
-
]
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
const roles = await roleService.listRoles();
|
|
530
|
-
|
|
531
|
-
expect(roles).toHaveLength(2);
|
|
532
|
-
});
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
describe("createRole", () => {
|
|
536
|
-
it("should create a role", async () => {
|
|
537
|
-
mockExecute.mockResolvedValueOnce({
|
|
538
|
-
rows: [{ id: "custom",
|
|
539
|
-
name: "Custom Role",
|
|
540
|
-
is_admin: false,
|
|
541
|
-
default_permissions: null,
|
|
542
|
-
collection_permissions: null }]
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
const role = await roleService.createRole({
|
|
546
|
-
id: "custom",
|
|
547
|
-
name: "Custom Role",
|
|
548
|
-
defaultPermissions: null
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
expect(role.id).toBe("custom");
|
|
552
|
-
expect(role.name).toBe("Custom Role");
|
|
553
|
-
});
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
describe("updateRole", () => {
|
|
557
|
-
it("should update a role", async () => {
|
|
558
|
-
mockExecute
|
|
559
|
-
.mockResolvedValueOnce({ rows: [{ id: "admin",
|
|
560
|
-
name: "Admin",
|
|
561
|
-
is_admin: true,
|
|
562
|
-
default_permissions: null,
|
|
563
|
-
collection_permissions: null }] })
|
|
564
|
-
.mockResolvedValueOnce({ rows: [] })
|
|
565
|
-
.mockResolvedValueOnce({ rows: [{ id: "admin",
|
|
566
|
-
name: "Super Admin",
|
|
567
|
-
is_admin: true,
|
|
568
|
-
default_permissions: null,
|
|
569
|
-
collection_permissions: null }] });
|
|
570
|
-
|
|
571
|
-
const result = await roleService.updateRole("admin", { name: "Super Admin" });
|
|
572
|
-
|
|
573
|
-
expect(result?.name).toBe("Super Admin");
|
|
574
|
-
});
|
|
575
|
-
|
|
576
|
-
it("should return null when role not found", async () => {
|
|
577
|
-
mockExecute.mockResolvedValueOnce({ rows: [] });
|
|
578
|
-
|
|
579
|
-
const result = await roleService.updateRole("nonexistent", { name: "Test" });
|
|
580
|
-
|
|
581
|
-
expect(result).toBeNull();
|
|
582
|
-
});
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
describe("deleteRole", () => {
|
|
586
|
-
it("should delete a role", async () => {
|
|
587
|
-
await roleService.deleteRole("custom");
|
|
588
|
-
|
|
589
|
-
expect(mockExecute).toHaveBeenCalled();
|
|
590
|
-
});
|
|
591
|
-
});
|
|
592
|
-
});
|
|
593
|
-
|
|
594
451
|
describe("RefreshTokenService", () => {
|
|
595
452
|
let refreshTokenService: RefreshTokenService;
|
|
596
453
|
|
package/test/doctor.test.ts
CHANGED
|
@@ -135,8 +135,12 @@ columnType: "time" } as DateProperty)).toBe("time without time zone");
|
|
|
135
135
|
it("should map json types correctly", () => {
|
|
136
136
|
expect(getExpectedColumnType({ type: "map" })).toBe("jsonb");
|
|
137
137
|
expect(getExpectedColumnType({ type: "array" })).toBe("jsonb");
|
|
138
|
-
expect(getExpectedColumnType({ type: "array",
|
|
139
|
-
|
|
138
|
+
expect(getExpectedColumnType({ type: "array", columnType: "json" } as ArrayProperty)).toBe("json");
|
|
139
|
+
|
|
140
|
+
// Native array element type mappings
|
|
141
|
+
expect(getExpectedColumnType({ type: "array", of: { type: "string" } } as ArrayProperty)).toBe("ARRAY");
|
|
142
|
+
expect(getExpectedColumnType({ type: "array", of: { type: "number", validation: { integer: true } } } as ArrayProperty)).toBe("ARRAY");
|
|
143
|
+
expect(getExpectedColumnType({ type: "array", of: { type: "boolean" } } as ArrayProperty)).toBe("ARRAY");
|
|
140
144
|
});
|
|
141
145
|
|
|
142
146
|
it("should map enum string to USER-DEFINED", () => {
|