@tstdl/base 0.93.81 → 0.93.83

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.
@@ -1,7 +1,8 @@
1
1
  CREATE TYPE "authentication"."subject_type" AS ENUM('system', 'user', 'service-account');--> statement-breakpoint
2
2
  CREATE TYPE "authentication"."user_status" AS ENUM('active', 'suspended', 'pending-approval', 'invited');--> statement-breakpoint
3
3
  CREATE TABLE "authentication"."credentials" (
4
- "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
4
+ "id" uuid DEFAULT gen_random_uuid() NOT NULL,
5
+ "tenant_id" uuid NOT NULL,
5
6
  "subject" uuid NOT NULL,
6
7
  "hash_version" integer NOT NULL,
7
8
  "salt" "bytea" NOT NULL,
@@ -11,11 +12,13 @@ CREATE TABLE "authentication"."credentials" (
11
12
  "create_timestamp" timestamp with time zone NOT NULL,
12
13
  "delete_timestamp" timestamp with time zone,
13
14
  "attributes" jsonb DEFAULT '{}'::jsonb NOT NULL,
14
- CONSTRAINT "credentials_subject_unique" UNIQUE("subject")
15
+ CONSTRAINT "credentials_tenant_id_id_pk" PRIMARY KEY("tenant_id","id"),
16
+ CONSTRAINT "credentials_tenant_id_subject_unique" UNIQUE("tenant_id","subject")
15
17
  );
16
18
  --> statement-breakpoint
17
19
  CREATE TABLE "authentication"."session" (
18
- "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
20
+ "id" uuid DEFAULT gen_random_uuid() NOT NULL,
21
+ "tenant_id" uuid NOT NULL,
19
22
  "subject" uuid NOT NULL,
20
23
  "begin" timestamp with time zone NOT NULL,
21
24
  "end" timestamp with time zone NOT NULL,
@@ -26,7 +29,8 @@ CREATE TABLE "authentication"."session" (
26
29
  "revision_timestamp" timestamp with time zone NOT NULL,
27
30
  "create_timestamp" timestamp with time zone NOT NULL,
28
31
  "delete_timestamp" timestamp with time zone,
29
- "attributes" jsonb DEFAULT '{}'::jsonb NOT NULL
32
+ "attributes" jsonb DEFAULT '{}'::jsonb NOT NULL,
33
+ CONSTRAINT "session_tenant_id_id_pk" PRIMARY KEY("tenant_id","id")
30
34
  );
31
35
  --> statement-breakpoint
32
36
  CREATE TABLE "authentication"."service_account" (
@@ -56,9 +60,10 @@ CREATE TABLE "authentication"."subject" (
56
60
  "delete_timestamp" timestamp with time zone,
57
61
  "attributes" jsonb DEFAULT '{}'::jsonb NOT NULL,
58
62
  CONSTRAINT "subject_tenant_id_id_pk" PRIMARY KEY("tenant_id","id"),
59
- CONSTRAINT "subject_system_account_id_unique" UNIQUE("system_account_id"),
60
- CONSTRAINT "subject_user_id_unique" UNIQUE("user_id"),
61
- CONSTRAINT "subject_service_account_id_unique" UNIQUE("service_account_id"),
63
+ CONSTRAINT "subject_tenant_id_service_account_id_unique" UNIQUE("tenant_id","service_account_id"),
64
+ CONSTRAINT "subject_tenant_id_user_id_unique" UNIQUE("tenant_id","user_id"),
65
+ CONSTRAINT "subject_tenant_id_system_account_id_unique" UNIQUE("tenant_id","system_account_id"),
66
+ CONSTRAINT "subject_id_unique" UNIQUE("id"),
62
67
  CONSTRAINT "authentication_subject_reference_check" CHECK (num_nonnulls("authentication"."subject"."system_account_id", "authentication"."subject"."user_id", "authentication"."subject"."service_account_id") = 1)
63
68
  );
64
69
  --> statement-breakpoint
@@ -91,8 +96,8 @@ CREATE TABLE "authentication"."user" (
91
96
  CONSTRAINT "user_tenant_id_email_unique" UNIQUE("tenant_id","email")
92
97
  );
93
98
  --> statement-breakpoint
94
- ALTER TABLE "authentication"."credentials" ADD CONSTRAINT "credentials_subject_subject_id_fk" FOREIGN KEY ("subject") REFERENCES "authentication"."subject"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
95
- ALTER TABLE "authentication"."session" ADD CONSTRAINT "session_subject_subject_id_fk" FOREIGN KEY ("subject") REFERENCES "authentication"."subject"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
99
+ ALTER TABLE "authentication"."credentials" ADD CONSTRAINT "credentials_id_subject_fkey" FOREIGN KEY ("tenant_id","subject") REFERENCES "authentication"."subject"("tenant_id","id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
100
+ ALTER TABLE "authentication"."session" ADD CONSTRAINT "session_id_subject_fkey" FOREIGN KEY ("tenant_id","subject") REFERENCES "authentication"."subject"("tenant_id","id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
96
101
  ALTER TABLE "authentication"."service_account" ADD CONSTRAINT "service_account_id_subject_fkey" FOREIGN KEY ("tenant_id","parent") REFERENCES "authentication"."subject"("tenant_id","id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
97
102
  ALTER TABLE "authentication"."subject" ADD CONSTRAINT "subject_id_system_account_fkey" FOREIGN KEY ("tenant_id","system_account_id") REFERENCES "authentication"."system_account"("tenant_id","id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
98
103
  ALTER TABLE "authentication"."subject" ADD CONSTRAINT "subject_id_user_fkey" FOREIGN KEY ("tenant_id","user_id") REFERENCES "authentication"."user"("tenant_id","id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "c88f8c29-7029-4da0-b085-bd866a480533",
2
+ "id": "9385eff0-25d7-4ef6-be4b-8019af0a1424",
3
3
  "prevId": "00000000-0000-0000-0000-000000000000",
4
4
  "version": "7",
5
5
  "dialect": "postgresql",
@@ -11,10 +11,16 @@
11
11
  "id": {
12
12
  "name": "id",
13
13
  "type": "uuid",
14
- "primaryKey": true,
14
+ "primaryKey": false,
15
15
  "notNull": true,
16
16
  "default": "gen_random_uuid()"
17
17
  },
18
+ "tenant_id": {
19
+ "name": "tenant_id",
20
+ "type": "uuid",
21
+ "primaryKey": false,
22
+ "notNull": true
23
+ },
18
24
  "subject": {
19
25
  "name": "subject",
20
26
  "type": "uuid",
@@ -73,27 +79,38 @@
73
79
  },
74
80
  "indexes": {},
75
81
  "foreignKeys": {
76
- "credentials_subject_subject_id_fk": {
77
- "name": "credentials_subject_subject_id_fk",
82
+ "credentials_id_subject_fkey": {
83
+ "name": "credentials_id_subject_fkey",
78
84
  "tableFrom": "credentials",
79
85
  "tableTo": "subject",
80
86
  "schemaTo": "authentication",
81
87
  "columnsFrom": [
88
+ "tenant_id",
82
89
  "subject"
83
90
  ],
84
91
  "columnsTo": [
92
+ "tenant_id",
85
93
  "id"
86
94
  ],
87
95
  "onDelete": "no action",
88
96
  "onUpdate": "no action"
89
97
  }
90
98
  },
91
- "compositePrimaryKeys": {},
99
+ "compositePrimaryKeys": {
100
+ "credentials_tenant_id_id_pk": {
101
+ "name": "credentials_tenant_id_id_pk",
102
+ "columns": [
103
+ "tenant_id",
104
+ "id"
105
+ ]
106
+ }
107
+ },
92
108
  "uniqueConstraints": {
93
- "credentials_subject_unique": {
94
- "name": "credentials_subject_unique",
109
+ "credentials_tenant_id_subject_unique": {
110
+ "name": "credentials_tenant_id_subject_unique",
95
111
  "nullsNotDistinct": false,
96
112
  "columns": [
113
+ "tenant_id",
97
114
  "subject"
98
115
  ]
99
116
  }
@@ -109,10 +126,16 @@
109
126
  "id": {
110
127
  "name": "id",
111
128
  "type": "uuid",
112
- "primaryKey": true,
129
+ "primaryKey": false,
113
130
  "notNull": true,
114
131
  "default": "gen_random_uuid()"
115
132
  },
133
+ "tenant_id": {
134
+ "name": "tenant_id",
135
+ "type": "uuid",
136
+ "primaryKey": false,
137
+ "notNull": true
138
+ },
116
139
  "subject": {
117
140
  "name": "subject",
118
141
  "type": "uuid",
@@ -183,22 +206,32 @@
183
206
  },
184
207
  "indexes": {},
185
208
  "foreignKeys": {
186
- "session_subject_subject_id_fk": {
187
- "name": "session_subject_subject_id_fk",
209
+ "session_id_subject_fkey": {
210
+ "name": "session_id_subject_fkey",
188
211
  "tableFrom": "session",
189
212
  "tableTo": "subject",
190
213
  "schemaTo": "authentication",
191
214
  "columnsFrom": [
215
+ "tenant_id",
192
216
  "subject"
193
217
  ],
194
218
  "columnsTo": [
219
+ "tenant_id",
195
220
  "id"
196
221
  ],
197
222
  "onDelete": "no action",
198
223
  "onUpdate": "no action"
199
224
  }
200
225
  },
201
- "compositePrimaryKeys": {},
226
+ "compositePrimaryKeys": {
227
+ "session_tenant_id_id_pk": {
228
+ "name": "session_tenant_id_id_pk",
229
+ "columns": [
230
+ "tenant_id",
231
+ "id"
232
+ ]
233
+ }
234
+ },
202
235
  "uniqueConstraints": {},
203
236
  "policies": {},
204
237
  "checkConstraints": {},
@@ -439,25 +472,35 @@
439
472
  }
440
473
  },
441
474
  "uniqueConstraints": {
442
- "subject_system_account_id_unique": {
443
- "name": "subject_system_account_id_unique",
475
+ "subject_tenant_id_service_account_id_unique": {
476
+ "name": "subject_tenant_id_service_account_id_unique",
444
477
  "nullsNotDistinct": false,
445
478
  "columns": [
446
- "system_account_id"
479
+ "tenant_id",
480
+ "service_account_id"
447
481
  ]
448
482
  },
449
- "subject_user_id_unique": {
450
- "name": "subject_user_id_unique",
483
+ "subject_tenant_id_user_id_unique": {
484
+ "name": "subject_tenant_id_user_id_unique",
451
485
  "nullsNotDistinct": false,
452
486
  "columns": [
487
+ "tenant_id",
453
488
  "user_id"
454
489
  ]
455
490
  },
456
- "subject_service_account_id_unique": {
457
- "name": "subject_service_account_id_unique",
491
+ "subject_tenant_id_system_account_id_unique": {
492
+ "name": "subject_tenant_id_system_account_id_unique",
458
493
  "nullsNotDistinct": false,
459
494
  "columns": [
460
- "service_account_id"
495
+ "tenant_id",
496
+ "system_account_id"
497
+ ]
498
+ },
499
+ "subject_id_unique": {
500
+ "name": "subject_id_unique",
501
+ "nullsNotDistinct": false,
502
+ "columns": [
503
+ "id"
461
504
  ]
462
505
  }
463
506
  },
@@ -5,8 +5,8 @@
5
5
  {
6
6
  "idx": 0,
7
7
  "version": "7",
8
- "when": 1767826939668,
9
- "tag": "0000_violet_callisto",
8
+ "when": 1767838186408,
9
+ "tag": "0000_majestic_proudstar",
10
10
  "breakpoints": true
11
11
  }
12
12
  ]
@@ -1,6 +1,22 @@
1
- import { Subject, SystemAccount } from '../models/index.js';
1
+ import { ServiceAccount, Subject, User } from '../models/index.js';
2
+ export type CreateUser = Pick<User, 'tenantId' | 'email' | 'firstName' | 'lastName'> & Partial<Pick<User, 'status'>>;
3
+ export type CreateServiceAccount = Pick<ServiceAccount, 'tenantId' | 'description' | 'parent'>;
2
4
  export declare class SubjectService {
3
- readonly subjectRepository: import("../../orm/server/index.js").EntityRepository<Subject>;
4
- readonly systemAccountRepository: import("../../orm/server/index.js").EntityRepository<SystemAccount>;
5
+ #private;
6
+ getSubject(id: string): Promise<Subject>;
7
+ tryGetSubject(id: string): Promise<Subject | undefined>;
5
8
  getSystemAccountSubject(tenantId: string, identifier: string): Promise<Subject>;
9
+ createUser(data: CreateUser): Promise<User>;
10
+ updateUser(tenantId: string, userId: string, data: Partial<Pick<User, 'firstName' | 'lastName' | 'status'>>): Promise<void>;
11
+ updateUserEmail(tenantId: string, userId: string, email: string): Promise<void>;
12
+ getUserSubject(tenantId: string, userId: string): Promise<Subject>;
13
+ getUserByEmail(tenantId: string, email: string): Promise<User>;
14
+ tryGetUserByEmail(tenantId: string, email: string): Promise<User | undefined>;
15
+ hasUserByEmail(tenantId: string, email: string): Promise<boolean>;
16
+ getUserBySubject(subject: Subject): Promise<User>;
17
+ loadManyUsersByEmails(tenantId: string, emails: string[]): Promise<User[]>;
18
+ createServiceAccount(data: CreateServiceAccount): Promise<ServiceAccount>;
19
+ updateServiceAccount(tenantId: string, serviceAccountId: string, data: Partial<Pick<ServiceAccount, 'description'>> & Partial<Pick<Subject, 'displayName'>>): Promise<void>;
20
+ getServiceAccountSubject(tenantId: string, serviceAccountId: string): Promise<Subject>;
21
+ getServiceAccountBySubject(subject: Subject): Promise<ServiceAccount>;
6
22
  }
@@ -4,25 +4,36 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
+ import { BadRequestError } from '../../errors/bad-request.error.js';
8
+ import { formatPersonName } from '../../formats/formats.js';
7
9
  import { Singleton } from '../../injector/index.js';
8
10
  import { injectRepository } from '../../orm/server/index.js';
9
- import { isUndefined } from '../../utils/type-guards.js';
10
- import { Subject, SubjectType, SystemAccount } from '../models/index.js';
11
+ import { mailPattern } from '../../utils/patterns.js';
12
+ import { assertNotNull, isDefined, isUndefined } from '../../utils/type-guards.js';
13
+ import { ServiceAccount, Subject, SubjectType, SystemAccount, User, UserStatus } from '../models/index.js';
11
14
  let SubjectService = class SubjectService {
12
- subjectRepository = injectRepository(Subject);
13
- systemAccountRepository = injectRepository(SystemAccount);
15
+ #subjectRepository = injectRepository(Subject);
16
+ #systemAccountRepository = injectRepository(SystemAccount);
17
+ #userRepository = injectRepository(User);
18
+ #serviceAccountRepository = injectRepository(ServiceAccount);
19
+ async getSubject(id) {
20
+ return await this.#subjectRepository.load(id);
21
+ }
22
+ async tryGetSubject(id) {
23
+ return await this.#subjectRepository.tryLoad(id);
24
+ }
14
25
  async getSystemAccountSubject(tenantId, identifier) {
15
- return await this.subjectRepository.transaction(async (tx) => {
16
- let systemAccount = await this.systemAccountRepository.withTransaction(tx).tryLoadByQuery({
26
+ return await this.#subjectRepository.transaction(async (tx) => {
27
+ let systemAccount = await this.#systemAccountRepository.withTransaction(tx).tryLoadByQuery({
17
28
  tenantId,
18
29
  identifier,
19
30
  });
20
31
  if (isUndefined(systemAccount)) {
21
- systemAccount = await this.systemAccountRepository.withTransaction(tx).insert({
32
+ systemAccount = await this.#systemAccountRepository.withTransaction(tx).insert({
22
33
  tenantId,
23
34
  identifier,
24
35
  });
25
- return await this.subjectRepository.withTransaction(tx).insert({
36
+ return await this.#subjectRepository.withTransaction(tx).insert({
26
37
  type: SubjectType.System,
27
38
  tenantId,
28
39
  systemAccountId: systemAccount.id,
@@ -31,12 +42,118 @@ let SubjectService = class SubjectService {
31
42
  serviceAccountId: null,
32
43
  });
33
44
  }
34
- return await this.subjectRepository.withTransaction(tx).loadByQuery({
45
+ return await this.#subjectRepository.withTransaction(tx).loadByQuery({
35
46
  tenantId,
36
47
  systemAccountId: systemAccount.id,
37
48
  });
38
49
  });
39
50
  }
51
+ async createUser(data) {
52
+ const { tenantId, email, firstName, lastName, status } = data;
53
+ return await this.#userRepository.transaction(async (tx) => {
54
+ const user = await this.#userRepository.withTransaction(tx).insert({
55
+ tenantId,
56
+ email,
57
+ firstName,
58
+ lastName,
59
+ status: status ?? UserStatus.Active,
60
+ });
61
+ await this.#subjectRepository.withTransaction(tx).insert({
62
+ tenantId,
63
+ type: SubjectType.User,
64
+ displayName: `${firstName} ${lastName}`,
65
+ systemAccountId: null,
66
+ userId: user.id,
67
+ serviceAccountId: null,
68
+ });
69
+ return user;
70
+ });
71
+ }
72
+ async updateUser(tenantId, userId, data) {
73
+ const { firstName, lastName, status } = data;
74
+ await this.#userRepository.transaction(async (tx) => {
75
+ const updateData = {};
76
+ if (isDefined(firstName)) {
77
+ updateData.firstName = firstName;
78
+ }
79
+ if (isDefined(lastName)) {
80
+ updateData.lastName = lastName;
81
+ }
82
+ if (isDefined(status)) {
83
+ updateData.status = status;
84
+ }
85
+ if (Object.keys(updateData).length > 0) {
86
+ const updatedUser = await this.#userRepository.withTransaction(tx).updateByQuery({ tenantId, userId }, updateData);
87
+ if (isDefined(firstName) || isDefined(lastName)) {
88
+ await this.#subjectRepository.withTransaction(tx).updateByQuery({ tenantId, userId }, { displayName: formatPersonName(updatedUser) });
89
+ }
90
+ }
91
+ });
92
+ }
93
+ async updateUserEmail(tenantId, userId, email) {
94
+ // TODO: future (out of scope right now): validate mail flow
95
+ if (!mailPattern.test(email)) {
96
+ throw new BadRequestError(`Invalid email format.`);
97
+ }
98
+ await this.#userRepository.updateByQuery({ tenantId, userId }, { email });
99
+ }
100
+ async getUserSubject(tenantId, userId) {
101
+ return await this.#subjectRepository.loadByQuery({ tenantId, userId });
102
+ }
103
+ async getUserByEmail(tenantId, email) {
104
+ return await this.#userRepository.loadByQuery({ tenantId, email });
105
+ }
106
+ async tryGetUserByEmail(tenantId, email) {
107
+ return await this.#userRepository.tryLoadByQuery({ tenantId, email });
108
+ }
109
+ async hasUserByEmail(tenantId, email) {
110
+ const user = await this.tryGetUserByEmail(tenantId, email);
111
+ return isDefined(user);
112
+ }
113
+ async getUserBySubject(subject) {
114
+ assertNotNull(subject.userId, 'Subject is not a user subject');
115
+ return await this.#userRepository.load(subject.userId);
116
+ }
117
+ async loadManyUsersByEmails(tenantId, emails) {
118
+ return await this.#userRepository.loadManyByQuery({ tenantId, email: { $in: emails } });
119
+ }
120
+ async createServiceAccount(data) {
121
+ const { tenantId, description, parent } = data;
122
+ return await this.#serviceAccountRepository.transaction(async (tx) => {
123
+ const serviceAccount = await this.#serviceAccountRepository.withTransaction(tx).insert({
124
+ tenantId,
125
+ description,
126
+ parent,
127
+ });
128
+ await this.#subjectRepository.withTransaction(tx).insert({
129
+ tenantId,
130
+ type: SubjectType.ServiceAccount,
131
+ displayName: description,
132
+ userId: null,
133
+ systemAccountId: null,
134
+ serviceAccountId: serviceAccount.id,
135
+ });
136
+ return serviceAccount;
137
+ });
138
+ }
139
+ async updateServiceAccount(tenantId, serviceAccountId, data) {
140
+ const { displayName, description } = data;
141
+ await this.#subjectRepository.transaction(async (tx) => {
142
+ if (isDefined(displayName)) {
143
+ await this.#subjectRepository.withTransaction(tx).updateByQuery({ tenantId, serviceAccountId }, { displayName });
144
+ }
145
+ if (isDefined(description)) {
146
+ await this.#serviceAccountRepository.withTransaction(tx).updateByQuery({ tenantId, serviceAccountId }, { description });
147
+ }
148
+ });
149
+ }
150
+ async getServiceAccountSubject(tenantId, serviceAccountId) {
151
+ return await this.#subjectRepository.loadByQuery({ tenantId, serviceAccountId });
152
+ }
153
+ async getServiceAccountBySubject(subject) {
154
+ assertNotNull(subject.serviceAccountId, 'Subject is not a service account subject');
155
+ return await this.#serviceAccountRepository.load(subject.serviceAccountId);
156
+ }
40
157
  };
41
158
  SubjectService = __decorate([
42
159
  Singleton()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tstdl/base",
3
- "version": "0.93.81",
3
+ "version": "0.93.83",
4
4
  "author": "Patrick Hein",
5
5
  "publishConfig": {
6
6
  "access": "public"