@loomcore/api 0.1.53 → 0.1.57

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.
Files changed (96) hide show
  1. package/dist/__tests__/common-test.utils.d.ts +8 -2
  2. package/dist/__tests__/common-test.utils.js +50 -16
  3. package/dist/__tests__/index.d.ts +0 -1
  4. package/dist/__tests__/index.js +0 -1
  5. package/dist/__tests__/postgres-test-migrations/postgres-test-schema.d.ts +3 -0
  6. package/dist/__tests__/postgres-test-migrations/postgres-test-schema.js +97 -0
  7. package/dist/__tests__/postgres.test-database.js +9 -8
  8. package/dist/__tests__/setup/vitest-setup.js +15 -0
  9. package/dist/__tests__/test-objects.d.ts +6 -3
  10. package/dist/__tests__/test-objects.js +10 -2
  11. package/dist/controllers/api.controller.d.ts +2 -0
  12. package/dist/controllers/api.controller.js +66 -11
  13. package/dist/databases/migrations/migration-runner.js +6 -6
  14. package/dist/databases/models/database.interface.d.ts +9 -8
  15. package/dist/databases/mongo-db/commands/mongo-delete-by-id.command.d.ts +2 -1
  16. package/dist/databases/mongo-db/commands/mongo-full-updateby-id.command.d.ts +2 -1
  17. package/dist/databases/mongo-db/commands/mongo-partial-update-by-id.command.d.ts +2 -1
  18. package/dist/databases/mongo-db/migrations/{mongo-foundational.d.ts → mongo-initial-schema.d.ts} +1 -1
  19. package/dist/databases/mongo-db/migrations/mongo-initial-schema.js +264 -0
  20. package/dist/databases/mongo-db/mongo-db.database.d.ts +9 -8
  21. package/dist/databases/mongo-db/mongo-db.database.js +2 -2
  22. package/dist/databases/mongo-db/queries/mongo-get-by-id.query.d.ts +2 -1
  23. package/dist/databases/postgres/commands/postgres-batch-update.command.js +1 -1
  24. package/dist/databases/postgres/commands/postgres-create-many.command.d.ts +2 -1
  25. package/dist/databases/postgres/commands/postgres-create-many.command.js +23 -28
  26. package/dist/databases/postgres/commands/postgres-create.command.d.ts +2 -1
  27. package/dist/databases/postgres/commands/postgres-create.command.js +6 -5
  28. package/dist/databases/postgres/commands/postgres-delete-by-id.command.d.ts +2 -1
  29. package/dist/databases/postgres/commands/postgres-full-update-by-id.command.d.ts +2 -1
  30. package/dist/databases/postgres/commands/postgres-partial-update-by-id.command.d.ts +2 -1
  31. package/dist/databases/postgres/migrations/__tests__/test-migration-helper.d.ts +4 -0
  32. package/dist/databases/postgres/migrations/__tests__/test-migration-helper.js +67 -0
  33. package/dist/databases/postgres/migrations/index.d.ts +1 -2
  34. package/dist/databases/postgres/migrations/index.js +1 -2
  35. package/dist/databases/postgres/migrations/{postgres-foundational.d.ts → postgres-initial-schema.d.ts} +1 -1
  36. package/dist/databases/postgres/migrations/postgres-initial-schema.js +436 -0
  37. package/dist/databases/postgres/postgres.database.d.ts +10 -9
  38. package/dist/databases/postgres/postgres.database.js +2 -2
  39. package/dist/databases/postgres/queries/postgres-get-by-id.query.d.ts +2 -1
  40. package/dist/models/refresh-token.model.d.ts +5 -4
  41. package/dist/models/refresh-token.model.js +3 -3
  42. package/dist/services/auth.service.d.ts +3 -2
  43. package/dist/services/auth.service.js +4 -4
  44. package/dist/services/generic-api-service/generic-api-service.interface.d.ts +8 -7
  45. package/dist/services/generic-api-service/generic-api.service.d.ts +8 -7
  46. package/dist/services/generic-api-service/generic-api.service.js +23 -23
  47. package/dist/services/multi-tenant-api.service.d.ts +1 -1
  48. package/dist/services/multi-tenant-api.service.js +3 -3
  49. package/dist/services/organization.service.d.ts +4 -3
  50. package/dist/services/organization.service.js +3 -3
  51. package/dist/services/user.service.d.ts +3 -2
  52. package/dist/services/user.service.js +2 -2
  53. package/dist/services/utils/audit-for-create.util.js +2 -1
  54. package/dist/services/utils/audit-for-update.util.js +2 -1
  55. package/dist/services/utils/strip-sender-provided-system-properties.util.js +2 -1
  56. package/package.json +2 -2
  57. package/dist/__tests__/postgres-test-migrations/100-create-test-entities-table.migration.d.ts +0 -21
  58. package/dist/__tests__/postgres-test-migrations/100-create-test-entities-table.migration.js +0 -61
  59. package/dist/__tests__/postgres-test-migrations/101-create-categories-table.migration.d.ts +0 -21
  60. package/dist/__tests__/postgres-test-migrations/101-create-categories-table.migration.js +0 -51
  61. package/dist/__tests__/postgres-test-migrations/102-create-products-table.migration.d.ts +0 -21
  62. package/dist/__tests__/postgres-test-migrations/102-create-products-table.migration.js +0 -60
  63. package/dist/__tests__/postgres-test-migrations/103-create-test-items-table.migration.d.ts +0 -21
  64. package/dist/__tests__/postgres-test-migrations/103-create-test-items-table.migration.js +0 -59
  65. package/dist/__tests__/postgres-test-migrations/test-migrations.d.ts +0 -3
  66. package/dist/__tests__/postgres-test-migrations/test-migrations.js +0 -12
  67. package/dist/databases/mongo-db/migrations/mongo-foundational.js +0 -30
  68. package/dist/databases/postgres/migrations/001-create-migrations-table.migration.d.ts +0 -21
  69. package/dist/databases/postgres/migrations/001-create-migrations-table.migration.js +0 -51
  70. package/dist/databases/postgres/migrations/002-create-organizations-table.migration.d.ts +0 -21
  71. package/dist/databases/postgres/migrations/002-create-organizations-table.migration.js +0 -76
  72. package/dist/databases/postgres/migrations/003-create-users-table.migration.d.ts +0 -21
  73. package/dist/databases/postgres/migrations/003-create-users-table.migration.js +0 -77
  74. package/dist/databases/postgres/migrations/004-create-refresh-tokens-table.migration.d.ts +0 -21
  75. package/dist/databases/postgres/migrations/004-create-refresh-tokens-table.migration.js +0 -72
  76. package/dist/databases/postgres/migrations/005-create-meta-org.migration.d.ts +0 -22
  77. package/dist/databases/postgres/migrations/005-create-meta-org.migration.js +0 -64
  78. package/dist/databases/postgres/migrations/006-create-admin-user.migration.d.ts +0 -19
  79. package/dist/databases/postgres/migrations/006-create-admin-user.migration.js +0 -52
  80. package/dist/databases/postgres/migrations/007-create-roles-table.migration.d.ts +0 -21
  81. package/dist/databases/postgres/migrations/007-create-roles-table.migration.js +0 -65
  82. package/dist/databases/postgres/migrations/008-create-user-roles-table.migration.d.ts +0 -21
  83. package/dist/databases/postgres/migrations/008-create-user-roles-table.migration.js +0 -75
  84. package/dist/databases/postgres/migrations/009-create-features-table.migration.d.ts +0 -21
  85. package/dist/databases/postgres/migrations/009-create-features-table.migration.js +0 -65
  86. package/dist/databases/postgres/migrations/010-create-authorizations-table.migration.d.ts +0 -21
  87. package/dist/databases/postgres/migrations/010-create-authorizations-table.migration.js +0 -77
  88. package/dist/databases/postgres/migrations/011-create-admin-authorization.migration.d.ts +0 -23
  89. package/dist/databases/postgres/migrations/011-create-admin-authorization.migration.js +0 -130
  90. package/dist/databases/postgres/migrations/database-builder.d.ts +0 -15
  91. package/dist/databases/postgres/migrations/database-builder.interface.d.ts +0 -10
  92. package/dist/databases/postgres/migrations/database-builder.js +0 -65
  93. package/dist/databases/postgres/migrations/migration.interface.d.ts +0 -11
  94. package/dist/databases/postgres/migrations/migration.interface.js +0 -1
  95. package/dist/databases/postgres/migrations/postgres-foundational.js +0 -47
  96. /package/dist/{databases/postgres/migrations/database-builder.interface.js → __tests__/setup/vitest-setup.d.ts} +0 -0
@@ -0,0 +1,436 @@
1
+ import { initializeSystemUserContext, EmptyUserContext, getSystemUserContext } from '@loomcore/common/models';
2
+ import { PostgresDatabase } from '../postgres.database.js';
3
+ import { AuthService, OrganizationService } from '../../../services/index.js';
4
+ export const getPostgresInitialSchema = (config) => {
5
+ const migrations = [];
6
+ const isMultiTenant = config.app.isMultiTenant === true;
7
+ if (isMultiTenant) {
8
+ migrations.push({
9
+ name: '00000000000001_schema-organizations',
10
+ up: async ({ context: pool }) => {
11
+ await pool.query(`
12
+ CREATE TABLE IF NOT EXISTS "organizations" (
13
+ "_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
14
+ "name" VARCHAR(255) NOT NULL UNIQUE,
15
+ "code" VARCHAR(255) NOT NULL UNIQUE,
16
+ "description" TEXT,
17
+ "status" INTEGER NOT NULL,
18
+ "isMetaOrg" BOOLEAN NOT NULL,
19
+ "authToken" TEXT,
20
+ "_created" TIMESTAMPTZ NOT NULL,
21
+ "_createdBy" INTEGER NOT NULL,
22
+ "_updated" TIMESTAMPTZ NOT NULL,
23
+ "_updatedBy" INTEGER NOT NULL,
24
+ "_deleted" TIMESTAMPTZ,
25
+ "_deletedBy" INTEGER
26
+ )
27
+ `);
28
+ },
29
+ down: async ({ context: pool }) => {
30
+ await pool.query('DROP TABLE IF EXISTS "organizations"');
31
+ }
32
+ });
33
+ }
34
+ migrations.push({
35
+ name: '00000000000002_schema-users',
36
+ up: async ({ context: pool }) => {
37
+ const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : '';
38
+ const uniqueConstraint = isMultiTenant
39
+ ? 'CONSTRAINT "uk_users_email" UNIQUE ("_orgId", "email")'
40
+ : 'CONSTRAINT "uk_users_email" UNIQUE ("email")';
41
+ await pool.query(`
42
+ CREATE TABLE IF NOT EXISTS "users" (
43
+ "_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
44
+ ${orgColumnDef}
45
+ "email" VARCHAR(255) NOT NULL,
46
+ "firstName" VARCHAR(255),
47
+ "lastName" VARCHAR(255),
48
+ "displayName" VARCHAR(255),
49
+ "password" VARCHAR(255) NOT NULL,
50
+ "_lastLoggedIn" TIMESTAMPTZ,
51
+ "_lastPasswordChange" TIMESTAMPTZ,
52
+ "_created" TIMESTAMPTZ NOT NULL,
53
+ "_createdBy" INTEGER NOT NULL,
54
+ "_updated" TIMESTAMPTZ NOT NULL,
55
+ "_updatedBy" INTEGER NOT NULL,
56
+ "_deleted" TIMESTAMPTZ,
57
+ "_deletedBy" INTEGER,
58
+ ${uniqueConstraint}
59
+ )
60
+ `);
61
+ },
62
+ down: async ({ context: pool }) => {
63
+ await pool.query('DROP TABLE IF EXISTS "users"');
64
+ }
65
+ });
66
+ migrations.push({
67
+ name: '00000000000003_schema-refresh-tokens',
68
+ up: async ({ context: pool }) => {
69
+ const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : '';
70
+ await pool.query(`
71
+ CREATE TABLE IF NOT EXISTS "refreshTokens" (
72
+ "_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
73
+ ${orgColumnDef}
74
+ "token" VARCHAR(255) NOT NULL,
75
+ "deviceId" VARCHAR(255) NOT NULL,
76
+ "userId" INTEGER NOT NULL,
77
+ "expiresOn" BIGINT NOT NULL,
78
+ "created" TIMESTAMPTZ NOT NULL,
79
+ "createdBy" INTEGER NOT NULL,
80
+ CONSTRAINT "fk_refreshTokens_user" FOREIGN KEY ("userId") REFERENCES "users"("_id") ON DELETE CASCADE
81
+ )
82
+ `);
83
+ },
84
+ down: async ({ context: pool }) => {
85
+ await pool.query('DROP TABLE IF EXISTS "refreshTokens"');
86
+ }
87
+ });
88
+ migrations.push({
89
+ name: '00000000000004_schema-roles',
90
+ up: async ({ context: pool }) => {
91
+ const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : '';
92
+ const uniqueConstraint = isMultiTenant
93
+ ? 'CONSTRAINT "uk_roles_name" UNIQUE ("_orgId", "name")'
94
+ : 'CONSTRAINT "uk_roles_name" UNIQUE ("name")';
95
+ await pool.query(`
96
+ CREATE TABLE IF NOT EXISTS "roles" (
97
+ "_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
98
+ ${orgColumnDef}
99
+ "name" VARCHAR(255) NOT NULL,
100
+ "description" TEXT,
101
+ ${uniqueConstraint}
102
+ )
103
+ `);
104
+ },
105
+ down: async ({ context: pool }) => {
106
+ await pool.query('DROP TABLE IF EXISTS "roles"');
107
+ }
108
+ });
109
+ migrations.push({
110
+ name: '00000000000005_schema-user-roles',
111
+ up: async ({ context: pool }) => {
112
+ const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : '';
113
+ const uniqueConstraint = isMultiTenant
114
+ ? 'CONSTRAINT "uk_user_roles" UNIQUE ("_orgId", "userId", "roleId")'
115
+ : 'CONSTRAINT "uk_user_roles" UNIQUE ("userId", "roleId")';
116
+ await pool.query(`
117
+ CREATE TABLE IF NOT EXISTS "user_roles" (
118
+ "_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
119
+ ${orgColumnDef}
120
+ "userId" INTEGER NOT NULL,
121
+ "roleId" INTEGER NOT NULL,
122
+ "_created" TIMESTAMPTZ NOT NULL,
123
+ "_createdBy" INTEGER NOT NULL,
124
+ "_updated" TIMESTAMPTZ NOT NULL,
125
+ "_updatedBy" INTEGER NOT NULL,
126
+ "_deleted" TIMESTAMPTZ,
127
+ "_deletedBy" INTEGER,
128
+ CONSTRAINT "fk_user_roles_user" FOREIGN KEY ("userId") REFERENCES "users"("_id") ON DELETE CASCADE,
129
+ CONSTRAINT "fk_user_roles_role" FOREIGN KEY ("roleId") REFERENCES "roles"("_id") ON DELETE CASCADE,
130
+ ${uniqueConstraint}
131
+ )
132
+ `);
133
+ },
134
+ down: async ({ context: pool }) => {
135
+ await pool.query('DROP TABLE IF EXISTS "user_roles"');
136
+ }
137
+ });
138
+ migrations.push({
139
+ name: '00000000000006_schema-features',
140
+ up: async ({ context: pool }) => {
141
+ const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : '';
142
+ const uniqueConstraint = isMultiTenant
143
+ ? 'CONSTRAINT "uk_features" UNIQUE ("_orgId", "name")'
144
+ : 'CONSTRAINT "uk_features" UNIQUE ("name")';
145
+ await pool.query(`
146
+ CREATE TABLE IF NOT EXISTS "features" (
147
+ "_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
148
+ ${orgColumnDef}
149
+ "name" VARCHAR(255) NOT NULL,
150
+ "description" TEXT,
151
+ ${uniqueConstraint}
152
+ )
153
+ `);
154
+ },
155
+ down: async ({ context: pool }) => {
156
+ await pool.query('DROP TABLE IF EXISTS "features"');
157
+ }
158
+ });
159
+ migrations.push({
160
+ name: '00000000000007_schema-authorizations',
161
+ up: async ({ context: pool }) => {
162
+ const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : '';
163
+ const uniqueConstraint = isMultiTenant
164
+ ? 'CONSTRAINT "uk_authorizations" UNIQUE ("_orgId", "roleId", "featureId")'
165
+ : 'CONSTRAINT "uk_authorizations" UNIQUE ("roleId", "featureId")';
166
+ await pool.query(`
167
+ CREATE TABLE IF NOT EXISTS "authorizations" (
168
+ "_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
169
+ ${orgColumnDef}
170
+ "roleId" INTEGER NOT NULL,
171
+ "featureId" INTEGER NOT NULL,
172
+ "startDate" TIMESTAMPTZ,
173
+ "endDate" TIMESTAMPTZ,
174
+ "config" JSONB,
175
+ "_created" TIMESTAMPTZ NOT NULL,
176
+ "_createdBy" INTEGER NOT NULL,
177
+ "_updated" TIMESTAMPTZ NOT NULL,
178
+ "_updatedBy" INTEGER NOT NULL,
179
+ "_deleted" TIMESTAMPTZ,
180
+ "_deletedBy" INTEGER,
181
+ CONSTRAINT "fk_authorizations_role" FOREIGN KEY ("roleId") REFERENCES "roles"("_id") ON DELETE CASCADE,
182
+ CONSTRAINT "fk_authorizations_feature" FOREIGN KEY ("featureId") REFERENCES "features"("_id") ON DELETE CASCADE,
183
+ ${uniqueConstraint}
184
+ )
185
+ `);
186
+ },
187
+ down: async ({ context: pool }) => {
188
+ await pool.query('DROP TABLE IF EXISTS "authorizations"');
189
+ }
190
+ });
191
+ if (isMultiTenant && config.app.metaOrgName && config.app.metaOrgCode) {
192
+ migrations.push({
193
+ name: '00000000000008_data-meta-org',
194
+ up: async ({ context: pool }) => {
195
+ const result = await pool.query(`
196
+ INSERT INTO "organizations" ("name", "code", "status", "isMetaOrg", "_created", "_createdBy", "_updated", "_updatedBy")
197
+ VALUES ($1, $2, 1, true, NOW(), 0, NOW(), 0)
198
+ RETURNING "_id", "name", "code", "status", "isMetaOrg", "_created", "_createdBy", "_updated", "_updatedBy"
199
+ `, [config.app.metaOrgName, config.app.metaOrgCode]);
200
+ if (result.rowCount === 0) {
201
+ throw new Error('Failed to create meta organization');
202
+ }
203
+ initializeSystemUserContext(config.email?.systemEmailAddress || 'system@example.com', result.rows[0]);
204
+ },
205
+ down: async ({ context: pool }) => {
206
+ await pool.query(`DELETE FROM "organizations" WHERE "isMetaOrg" = TRUE`);
207
+ }
208
+ });
209
+ }
210
+ if (config.adminUser?.email && config.adminUser?.password) {
211
+ migrations.push({
212
+ name: '00000000000009_data-admin-user',
213
+ up: async ({ context: pool }) => {
214
+ if (!config.adminUser?.email || !config.adminUser?.password) {
215
+ throw new Error('Admin user email and password must be provided in config');
216
+ }
217
+ const client = await pool.connect();
218
+ try {
219
+ const database = new PostgresDatabase(client);
220
+ const authService = new AuthService(database);
221
+ let systemUserContext = getSystemUserContext();
222
+ if (!systemUserContext) {
223
+ throw new Error('SystemUserContext has not been initialized. For non-multi-tenant setups, initialize it before running migrations.');
224
+ }
225
+ if (isMultiTenant) {
226
+ if (!systemUserContext.organization?._id) {
227
+ const organizationService = new OrganizationService(database);
228
+ const metaOrg = await organizationService.getMetaOrg(EmptyUserContext);
229
+ if (metaOrg) {
230
+ initializeSystemUserContext(config.email?.systemEmailAddress || 'system@example.com', metaOrg);
231
+ systemUserContext = getSystemUserContext();
232
+ }
233
+ if (!systemUserContext?.organization?._id) {
234
+ throw new Error('Cannot create admin user: Multi-tenant mode is enabled but meta-org does not exist. Ensure metaOrgName and metaOrgCode are provided in config so the meta-org migration runs before the admin-user migration.');
235
+ }
236
+ }
237
+ }
238
+ const userData = {
239
+ email: config.adminUser.email,
240
+ password: config.adminUser.password,
241
+ firstName: 'Admin',
242
+ lastName: 'User',
243
+ displayName: 'Admin User',
244
+ };
245
+ if (isMultiTenant && systemUserContext.organization?._id) {
246
+ userData._orgId = systemUserContext.organization._id;
247
+ }
248
+ await authService.createUser(systemUserContext, userData);
249
+ }
250
+ finally {
251
+ client.release();
252
+ }
253
+ },
254
+ down: async ({ context: pool }) => {
255
+ if (!config.adminUser?.email)
256
+ return;
257
+ const result = await pool.query(`DELETE FROM "users" WHERE "email" = $1`, [config.adminUser.email]);
258
+ }
259
+ });
260
+ }
261
+ if (config.adminUser?.email) {
262
+ migrations.push({
263
+ name: '00000000000010_data-admin-authorizations',
264
+ up: async ({ context: pool }) => {
265
+ if (!config.adminUser?.email) {
266
+ throw new Error('Admin user email not found in config');
267
+ }
268
+ const client = await pool.connect();
269
+ try {
270
+ const database = new PostgresDatabase(client);
271
+ const organizationService = new OrganizationService(database);
272
+ const authService = new AuthService(database);
273
+ const metaOrg = isMultiTenant ? await organizationService.getMetaOrg(EmptyUserContext) : undefined;
274
+ if (isMultiTenant && !metaOrg) {
275
+ throw new Error('Meta organization not found. Ensure meta-org migration ran successfully.');
276
+ }
277
+ const adminUser = await authService.getUserByEmail(config.adminUser.email);
278
+ if (!adminUser) {
279
+ throw new Error('Admin user not found. Ensure admin-user migration ran successfully.');
280
+ }
281
+ await client.query('BEGIN');
282
+ try {
283
+ const roleResult = isMultiTenant
284
+ ? await client.query(`
285
+ INSERT INTO "roles" ("_orgId", "name")
286
+ VALUES ($1, 'admin')
287
+ RETURNING "_id"
288
+ `, [metaOrg._id])
289
+ : await client.query(`
290
+ INSERT INTO "roles" ("name")
291
+ VALUES ('admin')
292
+ RETURNING "_id"
293
+ `);
294
+ if (roleResult.rowCount === 0) {
295
+ throw new Error('Failed to create admin role');
296
+ }
297
+ const roleId = roleResult.rows[0]._id;
298
+ const userRoleResult = isMultiTenant
299
+ ? await client.query(`
300
+ INSERT INTO "user_roles" ("_orgId", "userId", "roleId", "_created", "_createdBy", "_updated", "_updatedBy")
301
+ VALUES ($1, $2, $3, NOW(), 0, NOW(), 0)
302
+ `, [metaOrg._id, adminUser._id, roleId])
303
+ : await client.query(`
304
+ INSERT INTO "user_roles" ("userId", "roleId", "_created", "_createdBy", "_updated", "_updatedBy")
305
+ VALUES ($1, $2, NOW(), 0, NOW(), 0)
306
+ `, [adminUser._id, roleId]);
307
+ if (userRoleResult.rowCount === 0) {
308
+ throw new Error('Failed to create user role');
309
+ }
310
+ const featureResult = isMultiTenant
311
+ ? await client.query(`
312
+ INSERT INTO "features" ("_orgId", "name")
313
+ VALUES ($1, 'admin')
314
+ RETURNING "_id"
315
+ `, [metaOrg._id])
316
+ : await client.query(`
317
+ INSERT INTO "features" ("name")
318
+ VALUES ('admin')
319
+ RETURNING "_id"
320
+ `);
321
+ if (featureResult.rowCount === 0) {
322
+ throw new Error('Failed to create admin feature');
323
+ }
324
+ const featureId = featureResult.rows[0]._id;
325
+ const authorizationResult = isMultiTenant
326
+ ? await client.query(`
327
+ INSERT INTO "authorizations" (
328
+ "_orgId", "roleId", "featureId",
329
+ "_created", "_createdBy", "_updated", "_updatedBy"
330
+ )
331
+ VALUES ($1, $2, $3, NOW(), 0, NOW(), 0)
332
+ `, [metaOrg._id, roleId, featureId])
333
+ : await client.query(`
334
+ INSERT INTO "authorizations" (
335
+ "roleId", "featureId",
336
+ "_created", "_createdBy", "_updated", "_updatedBy"
337
+ )
338
+ VALUES ($1, $2, NOW(), 0, NOW(), 0)
339
+ `, [roleId, featureId]);
340
+ if (authorizationResult.rowCount === 0) {
341
+ throw new Error('Failed to create admin authorization');
342
+ }
343
+ await client.query('COMMIT');
344
+ }
345
+ catch (error) {
346
+ await client.query('ROLLBACK');
347
+ throw error;
348
+ }
349
+ }
350
+ finally {
351
+ client.release();
352
+ }
353
+ },
354
+ down: async ({ context: pool }) => {
355
+ const client = await pool.connect();
356
+ try {
357
+ const database = new PostgresDatabase(client);
358
+ const organizationService = new OrganizationService(database);
359
+ const metaOrg = isMultiTenant ? await organizationService.getMetaOrg(EmptyUserContext) : undefined;
360
+ if (isMultiTenant && !metaOrg)
361
+ return;
362
+ await client.query('BEGIN');
363
+ try {
364
+ if (isMultiTenant) {
365
+ await client.query(`
366
+ DELETE FROM "authorizations"
367
+ WHERE "_orgId" = $1
368
+ AND "featureId" IN (
369
+ SELECT "_id" FROM "features"
370
+ WHERE "_orgId" = $1 AND "name" = 'admin'
371
+ )
372
+ AND "roleId" IN (
373
+ SELECT "_id" FROM "roles"
374
+ WHERE "_orgId" = $1 AND "name" = 'admin'
375
+ )
376
+ `, [metaOrg._id]);
377
+ await client.query(`
378
+ DELETE FROM "features"
379
+ WHERE "_orgId" = $1 AND "name" = 'admin'
380
+ `, [metaOrg._id]);
381
+ await client.query(`
382
+ DELETE FROM "user_roles"
383
+ WHERE "_orgId" = $1
384
+ AND "roleId" IN (
385
+ SELECT "_id" FROM "roles"
386
+ WHERE "_orgId" = $1 AND "name" = 'admin'
387
+ )
388
+ `, [metaOrg._id]);
389
+ await client.query(`
390
+ DELETE FROM "roles"
391
+ WHERE "_orgId" = $1 AND "name" = 'admin'
392
+ `, [metaOrg._id]);
393
+ }
394
+ else {
395
+ await client.query(`
396
+ DELETE FROM "authorizations"
397
+ WHERE "featureId" IN (
398
+ SELECT "_id" FROM "features"
399
+ WHERE "name" = 'admin'
400
+ )
401
+ AND "roleId" IN (
402
+ SELECT "_id" FROM "roles"
403
+ WHERE "name" = 'admin'
404
+ )
405
+ `);
406
+ await client.query(`
407
+ DELETE FROM "features"
408
+ WHERE "name" = 'admin'
409
+ `);
410
+ await client.query(`
411
+ DELETE FROM "user_roles"
412
+ WHERE "roleId" IN (
413
+ SELECT "_id" FROM "roles"
414
+ WHERE "name" = 'admin'
415
+ )
416
+ `);
417
+ await client.query(`
418
+ DELETE FROM "roles"
419
+ WHERE "name" = 'admin'
420
+ `);
421
+ }
422
+ await client.query('COMMIT');
423
+ }
424
+ catch (error) {
425
+ await client.query('ROLLBACK');
426
+ throw error;
427
+ }
428
+ }
429
+ finally {
430
+ client.release();
431
+ }
432
+ }
433
+ });
434
+ }
435
+ return migrations;
436
+ };
@@ -1,4 +1,5 @@
1
1
  import { IQueryOptions, IModelSpec, IPagedResult, IEntity, IUserContextAuthorization } from "@loomcore/common/models";
2
+ import type { AppIdType } from "@loomcore/common/types";
2
3
  import { TSchema } from "@sinclair/typebox";
3
4
  import { DeleteResult, IDatabase } from "../models/index.js";
4
5
  import { Operation } from "../operations/operation.js";
@@ -6,27 +7,27 @@ import { Client } from 'pg';
6
7
  export declare class PostgresDatabase implements IDatabase {
7
8
  private client;
8
9
  constructor(client: Client);
9
- preprocessEntity<T extends IEntity>(entity: Partial<T>, modelSpec: TSchema): Partial<T>;
10
- postprocessEntity<T extends IEntity>(entity: T, modelSpec: TSchema): T;
10
+ preProcessEntity<T extends IEntity>(entity: Partial<T>, modelSpec: TSchema): Partial<T>;
11
+ postProcessEntity<T extends IEntity>(entity: T, modelSpec: TSchema): T;
11
12
  getAll<T extends IEntity>(operations: Operation[], pluralResourceName: string): Promise<T[]>;
12
13
  get<T extends IEntity>(operations: Operation[], queryOptions: IQueryOptions, modelSpec: IModelSpec, pluralResourceName: string): Promise<IPagedResult<T>>;
13
- getById<T extends IEntity>(operations: Operation[], queryObject: IQueryOptions, id: string, pluralResourceName: string): Promise<T | null>;
14
+ getById<T extends IEntity>(operations: Operation[], queryObject: IQueryOptions, id: AppIdType, pluralResourceName: string): Promise<T | null>;
14
15
  getCount(pluralResourceName: string): Promise<number>;
15
16
  create<T extends IEntity>(entity: Partial<T>, pluralResourceName: string): Promise<{
16
- insertedId: string;
17
+ insertedId: AppIdType;
17
18
  entity: T;
18
19
  }>;
19
20
  createMany<T extends IEntity>(entities: Partial<T>[], pluralResourceName: string): Promise<{
20
- insertedIds: string[];
21
+ insertedIds: AppIdType[];
21
22
  entities: T[];
22
23
  }>;
23
24
  batchUpdate<T extends IEntity>(entities: Partial<T>[], operations: Operation[], queryObject: IQueryOptions, pluralResourceName: string): Promise<T[]>;
24
- fullUpdateById<T extends IEntity>(operations: Operation[], id: string, entity: Partial<T>, pluralResourceName: string): Promise<T>;
25
- partialUpdateById<T extends IEntity>(operations: Operation[], id: string, entity: Partial<T>, pluralResourceName: string): Promise<T>;
25
+ fullUpdateById<T extends IEntity>(operations: Operation[], id: AppIdType, entity: Partial<T>, pluralResourceName: string): Promise<T>;
26
+ partialUpdateById<T extends IEntity>(operations: Operation[], id: AppIdType, entity: Partial<T>, pluralResourceName: string): Promise<T>;
26
27
  update<T extends IEntity>(queryObject: IQueryOptions, entity: Partial<T>, operations: Operation[], pluralResourceName: string): Promise<T[]>;
27
- deleteById(id: string, pluralResourceName: string): Promise<DeleteResult>;
28
+ deleteById(id: AppIdType, pluralResourceName: string): Promise<DeleteResult>;
28
29
  deleteMany(queryObject: IQueryOptions, pluralResourceName: string): Promise<DeleteResult>;
29
30
  find<T extends IEntity>(queryObject: IQueryOptions, pluralResourceName: string): Promise<T[]>;
30
31
  findOne<T extends IEntity>(queryObject: IQueryOptions, pluralResourceName: string): Promise<T | null>;
31
- getUserAuthorizations(userId: string, orgId?: string): Promise<IUserContextAuthorization[]>;
32
+ getUserAuthorizations(userId: AppIdType, orgId?: AppIdType): Promise<IUserContextAuthorization[]>;
32
33
  }
@@ -18,10 +18,10 @@ export class PostgresDatabase {
18
18
  constructor(client) {
19
19
  this.client = client;
20
20
  }
21
- preprocessEntity(entity, modelSpec) {
21
+ preProcessEntity(entity, modelSpec) {
22
22
  return entity;
23
23
  }
24
- postprocessEntity(entity, modelSpec) {
24
+ postProcessEntity(entity, modelSpec) {
25
25
  return convertNullToUndefined(entity, modelSpec);
26
26
  }
27
27
  async getAll(operations, pluralResourceName) {
@@ -1,4 +1,5 @@
1
1
  import { Client } from 'pg';
2
2
  import { Operation } from "../../operations/operation.js";
3
3
  import { IQueryOptions } from '@loomcore/common/models';
4
- export declare function getById<T>(client: Client, operations: Operation[], queryObject: IQueryOptions, id: string, pluralResourceName: string): Promise<T | null>;
4
+ import type { AppIdType } from '@loomcore/common/types';
5
+ export declare function getById<T>(client: Client, operations: Operation[], queryObject: IQueryOptions, id: AppIdType, pluralResourceName: string): Promise<T | null>;
@@ -1,18 +1,19 @@
1
1
  import { IEntity } from "@loomcore/common/models";
2
+ import type { AppIdType } from "@loomcore/common/types";
2
3
  export interface IRefreshToken extends IEntity {
3
4
  token: string;
4
5
  deviceId: string;
5
- userId: string;
6
+ userId: AppIdType;
6
7
  expiresOn: number;
7
8
  created: Date;
8
- createdBy: string;
9
+ createdBy: AppIdType;
9
10
  }
10
11
  export declare const refreshTokenSchema: import("@sinclair/typebox").TObject<{
11
12
  token: import("@sinclair/typebox").TString;
12
13
  deviceId: import("@sinclair/typebox").TString;
13
- userId: import("@sinclair/typebox").TString;
14
+ userId: import("@sinclair/typebox").TSchema;
14
15
  expiresOn: import("@sinclair/typebox").TNumber;
15
16
  created: import("@sinclair/typebox").TTransform<import("@sinclair/typebox").TString, Date>;
16
- createdBy: import("@sinclair/typebox").TString;
17
+ createdBy: import("@sinclair/typebox").TSchema;
17
18
  }>;
18
19
  export declare const refreshTokenModelSpec: import("@loomcore/common/models").IModelSpec<import("@sinclair/typebox").TSchema>;
@@ -1,13 +1,13 @@
1
1
  import { entityUtils } from "@loomcore/common/utils";
2
- import { TypeboxIsoDate } from "@loomcore/common/validation";
2
+ import { TypeboxIsoDate, getIdSchema } from "@loomcore/common/validation";
3
3
  import { Type } from "@sinclair/typebox";
4
4
  ;
5
5
  export const refreshTokenSchema = Type.Object({
6
6
  token: Type.String({ minLength: 1 }),
7
7
  deviceId: Type.String({ minLength: 1 }),
8
- userId: Type.String({ minLength: 1 }),
8
+ userId: getIdSchema(),
9
9
  expiresOn: Type.Number(),
10
10
  created: TypeboxIsoDate({ title: 'Created Date' }),
11
- createdBy: Type.String({ minLength: 1 })
11
+ createdBy: getIdSchema()
12
12
  });
13
13
  export const refreshTokenModelSpec = entityUtils.getModelSpec(refreshTokenSchema);
@@ -1,5 +1,6 @@
1
1
  import { Request, Response } from 'express';
2
2
  import { IUserContext, ITokenResponse, ILoginResponse, IUser } from '@loomcore/common/models';
3
+ import type { AppIdType } from '@loomcore/common/types';
3
4
  import { MultiTenantApiService } from './multi-tenant-api.service.js';
4
5
  import { UpdateResult } from '../databases/models/update-result.js';
5
6
  import { IRefreshToken } from '../models/refresh-token.model.js';
@@ -30,7 +31,7 @@ export declare class AuthService extends MultiTenantApiService<IUser> {
30
31
  expiresOn: number;
31
32
  }>;
32
33
  getActiveRefreshToken(refreshToken: string, deviceId: string): Promise<IRefreshToken | null>;
33
- createNewRefreshToken(userId: string, deviceId: string, orgId?: string): Promise<IRefreshToken | null>;
34
+ createNewRefreshToken(userId: AppIdType, deviceId: string, orgId?: AppIdType): Promise<IRefreshToken | null>;
34
35
  sendResetPasswordEmail(emailAddress: string): Promise<void>;
35
36
  resetPassword(email: string, passwordResetToken: string, password: string): Promise<UpdateResult>;
36
37
  deleteRefreshTokensForDevice(deviceId: string): Promise<import("../databases/models/delete-result.js").DeleteResult>;
@@ -42,6 +43,6 @@ export declare class AuthService extends MultiTenantApiService<IUser> {
42
43
  getExpiresOnFromSeconds(expiresInSeconds: number): number;
43
44
  getExpiresOnFromMinutes(expiresInMinutes: number): number;
44
45
  getExpiresOnFromDays(expiresInDays: number): number;
45
- preprocessEntity(userContext: IUserContext, entity: Partial<IUser>, isCreate: boolean, allowId: boolean): Promise<Partial<IUser>>;
46
+ preProcessEntity(userContext: IUserContext, entity: Partial<IUser>, isCreate: boolean, allowId: boolean): Promise<Partial<IUser>>;
46
47
  private updateLastLoggedIn;
47
48
  }
@@ -59,7 +59,7 @@ export class AuthService extends MultiTenantApiService {
59
59
  };
60
60
  this.updateLastLoggedIn(userContext.user._id)
61
61
  .catch(err => console.log(`Error updating lastLoggedIn: ${err}`));
62
- userContext.user = this.postprocessEntity(userContext, userContext.user);
62
+ userContext.user = this.postProcessEntity(userContext, userContext.user);
63
63
  loginResponse = { tokens: tokenResponse, userContext };
64
64
  }
65
65
  return loginResponse;
@@ -70,7 +70,7 @@ export class AuthService extends MultiTenantApiService {
70
70
  if (!rawUser) {
71
71
  return null;
72
72
  }
73
- return this.database.postprocessEntity(rawUser, this.modelSpec.fullSchema);
73
+ return this.database.postProcessEntity(rawUser, this.modelSpec.fullSchema);
74
74
  }
75
75
  async createUser(userContext, user) {
76
76
  if (userContext.user._id === 'system') {
@@ -241,7 +241,7 @@ export class AuthService extends MultiTenantApiService {
241
241
  getExpiresOnFromDays(expiresInDays) {
242
242
  return Date.now() + expiresInDays * 24 * 60 * 60 * 1000;
243
243
  }
244
- async preprocessEntity(userContext, entity, isCreate, allowId) {
244
+ async preProcessEntity(userContext, entity, isCreate, allowId) {
245
245
  if (entity.email) {
246
246
  entity.email = entity.email.toLowerCase();
247
247
  }
@@ -249,7 +249,7 @@ export class AuthService extends MultiTenantApiService {
249
249
  const hash = await passwordUtils.hashPassword(entity.password);
250
250
  entity.password = hash;
251
251
  }
252
- const preparedEntity = await super.preprocessEntity(userContext, entity, isCreate, allowId);
252
+ const preparedEntity = await super.preProcessEntity(userContext, entity, isCreate, allowId);
253
253
  return preparedEntity;
254
254
  }
255
255
  async updateLastLoggedIn(userId) {
@@ -1,5 +1,6 @@
1
1
  import { ValueError } from '@sinclair/typebox/errors';
2
2
  import { IUserContext, IEntity, IPagedResult, IQueryOptions } from '@loomcore/common/models';
3
+ import type { AppIdType } from '@loomcore/common/types';
3
4
  import { DeleteResult } from '../../databases/models/delete-result.js';
4
5
  import { Operation } from '../../databases/operations/operation.js';
5
6
  export interface IGenericApiService<T extends IEntity> {
@@ -9,20 +10,20 @@ export interface IGenericApiService<T extends IEntity> {
9
10
  queryObject: IQueryOptions;
10
11
  operations: Operation[];
11
12
  };
12
- preprocessEntity(userContext: IUserContext, entity: Partial<T>, isCreate: boolean, allowId: boolean): Promise<Partial<T>>;
13
- postprocessEntity(userContext: IUserContext, entity: T): T;
13
+ preProcessEntity(userContext: IUserContext, entity: Partial<T>, isCreate: boolean, allowId: boolean): Promise<Partial<T>>;
14
+ postProcessEntity(userContext: IUserContext, entity: T): T;
14
15
  getAll(userContext: IUserContext): Promise<T[]>;
15
16
  get(userContext: IUserContext, queryOptions: IQueryOptions): Promise<IPagedResult<T>>;
16
- getById(userContext: IUserContext, id: string): Promise<T>;
17
+ getById(userContext: IUserContext, id: AppIdType): Promise<T>;
17
18
  getCount(userContext: IUserContext): Promise<number>;
18
19
  create(userContext: IUserContext, entity: Partial<T>): Promise<T | null>;
19
20
  createMany(userContext: IUserContext, entities: Partial<T>[]): Promise<T[]>;
20
21
  batchUpdate(userContext: IUserContext, entities: Partial<T>[]): Promise<T[]>;
21
- fullUpdateById(userContext: IUserContext, id: string, entity: T): Promise<T>;
22
- partialUpdateById(userContext: IUserContext, id: string, entity: Partial<T>): Promise<T>;
23
- partialUpdateByIdWithoutPreAndPostProcessing(userContext: IUserContext, id: string, entity: T): Promise<T>;
22
+ fullUpdateById(userContext: IUserContext, id: AppIdType, entity: T): Promise<T>;
23
+ partialUpdateById(userContext: IUserContext, id: AppIdType, entity: Partial<T>): Promise<T>;
24
+ partialUpdateByIdWithoutPreAndPostProcessing(userContext: IUserContext, id: AppIdType, entity: T): Promise<T>;
24
25
  update(userContext: IUserContext, queryObject: IQueryOptions, entity: Partial<T>): Promise<T[]>;
25
- deleteById(userContext: IUserContext, id: string): Promise<DeleteResult>;
26
+ deleteById(userContext: IUserContext, id: AppIdType): Promise<DeleteResult>;
26
27
  deleteMany(userContext: IUserContext, queryObject: IQueryOptions): Promise<DeleteResult>;
27
28
  find(userContext: IUserContext, queryObject: IQueryOptions, options?: any): Promise<T[]>;
28
29
  findOne(userContext: IUserContext, queryObject: IQueryOptions, options?: any): Promise<T | null>;