@checkstack/catalog-backend 0.2.9 → 0.2.10
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/CHANGELOG.md +7 -0
- package/drizzle/0001_early_madrox.sql +13 -0
- package/drizzle/meta/0001_snapshot.json +309 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +11 -9
- package/src/index.ts +19 -2
- package/src/router.ts +108 -11
- package/src/schema.ts +20 -2
- package/src/services/entity-service.test.ts +0 -2
- package/src/services/entity-service.ts +38 -3
package/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
CREATE TYPE "contact_type" AS ENUM('user', 'mailbox');--> statement-breakpoint
|
|
2
|
+
CREATE TABLE "system_contacts" (
|
|
3
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
4
|
+
"system_id" text NOT NULL,
|
|
5
|
+
"type" "contact_type" NOT NULL,
|
|
6
|
+
"user_id" text,
|
|
7
|
+
"email" text,
|
|
8
|
+
"label" text,
|
|
9
|
+
"created_at" timestamp DEFAULT now() NOT NULL
|
|
10
|
+
);
|
|
11
|
+
--> statement-breakpoint
|
|
12
|
+
ALTER TABLE "system_contacts" ADD CONSTRAINT "system_contacts_system_id_systems_id_fk" FOREIGN KEY ("system_id") REFERENCES "systems"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
13
|
+
ALTER TABLE "systems" DROP COLUMN "owner";
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "72cd2f27-3627-475c-96f6-778b75a35418",
|
|
3
|
+
"prevId": "79e86bc7-76a2-429f-815c-2d6c263aab8f",
|
|
4
|
+
"version": "7",
|
|
5
|
+
"dialect": "postgresql",
|
|
6
|
+
"tables": {
|
|
7
|
+
"public.groups": {
|
|
8
|
+
"name": "groups",
|
|
9
|
+
"schema": "",
|
|
10
|
+
"columns": {
|
|
11
|
+
"id": {
|
|
12
|
+
"name": "id",
|
|
13
|
+
"type": "text",
|
|
14
|
+
"primaryKey": true,
|
|
15
|
+
"notNull": true
|
|
16
|
+
},
|
|
17
|
+
"name": {
|
|
18
|
+
"name": "name",
|
|
19
|
+
"type": "text",
|
|
20
|
+
"primaryKey": false,
|
|
21
|
+
"notNull": true
|
|
22
|
+
},
|
|
23
|
+
"metadata": {
|
|
24
|
+
"name": "metadata",
|
|
25
|
+
"type": "json",
|
|
26
|
+
"primaryKey": false,
|
|
27
|
+
"notNull": false,
|
|
28
|
+
"default": "'{}'::json"
|
|
29
|
+
},
|
|
30
|
+
"created_at": {
|
|
31
|
+
"name": "created_at",
|
|
32
|
+
"type": "timestamp",
|
|
33
|
+
"primaryKey": false,
|
|
34
|
+
"notNull": true,
|
|
35
|
+
"default": "now()"
|
|
36
|
+
},
|
|
37
|
+
"updated_at": {
|
|
38
|
+
"name": "updated_at",
|
|
39
|
+
"type": "timestamp",
|
|
40
|
+
"primaryKey": false,
|
|
41
|
+
"notNull": true,
|
|
42
|
+
"default": "now()"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"indexes": {},
|
|
46
|
+
"foreignKeys": {},
|
|
47
|
+
"compositePrimaryKeys": {},
|
|
48
|
+
"uniqueConstraints": {},
|
|
49
|
+
"policies": {},
|
|
50
|
+
"checkConstraints": {},
|
|
51
|
+
"isRLSEnabled": false
|
|
52
|
+
},
|
|
53
|
+
"public.system_contacts": {
|
|
54
|
+
"name": "system_contacts",
|
|
55
|
+
"schema": "",
|
|
56
|
+
"columns": {
|
|
57
|
+
"id": {
|
|
58
|
+
"name": "id",
|
|
59
|
+
"type": "text",
|
|
60
|
+
"primaryKey": true,
|
|
61
|
+
"notNull": true
|
|
62
|
+
},
|
|
63
|
+
"system_id": {
|
|
64
|
+
"name": "system_id",
|
|
65
|
+
"type": "text",
|
|
66
|
+
"primaryKey": false,
|
|
67
|
+
"notNull": true
|
|
68
|
+
},
|
|
69
|
+
"type": {
|
|
70
|
+
"name": "type",
|
|
71
|
+
"type": "contact_type",
|
|
72
|
+
"typeSchema": "public",
|
|
73
|
+
"primaryKey": false,
|
|
74
|
+
"notNull": true
|
|
75
|
+
},
|
|
76
|
+
"user_id": {
|
|
77
|
+
"name": "user_id",
|
|
78
|
+
"type": "text",
|
|
79
|
+
"primaryKey": false,
|
|
80
|
+
"notNull": false
|
|
81
|
+
},
|
|
82
|
+
"email": {
|
|
83
|
+
"name": "email",
|
|
84
|
+
"type": "text",
|
|
85
|
+
"primaryKey": false,
|
|
86
|
+
"notNull": false
|
|
87
|
+
},
|
|
88
|
+
"label": {
|
|
89
|
+
"name": "label",
|
|
90
|
+
"type": "text",
|
|
91
|
+
"primaryKey": false,
|
|
92
|
+
"notNull": false
|
|
93
|
+
},
|
|
94
|
+
"created_at": {
|
|
95
|
+
"name": "created_at",
|
|
96
|
+
"type": "timestamp",
|
|
97
|
+
"primaryKey": false,
|
|
98
|
+
"notNull": true,
|
|
99
|
+
"default": "now()"
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"indexes": {},
|
|
103
|
+
"foreignKeys": {
|
|
104
|
+
"system_contacts_system_id_systems_id_fk": {
|
|
105
|
+
"name": "system_contacts_system_id_systems_id_fk",
|
|
106
|
+
"tableFrom": "system_contacts",
|
|
107
|
+
"tableTo": "systems",
|
|
108
|
+
"columnsFrom": [
|
|
109
|
+
"system_id"
|
|
110
|
+
],
|
|
111
|
+
"columnsTo": [
|
|
112
|
+
"id"
|
|
113
|
+
],
|
|
114
|
+
"onDelete": "cascade",
|
|
115
|
+
"onUpdate": "no action"
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"compositePrimaryKeys": {},
|
|
119
|
+
"uniqueConstraints": {},
|
|
120
|
+
"policies": {},
|
|
121
|
+
"checkConstraints": {},
|
|
122
|
+
"isRLSEnabled": false
|
|
123
|
+
},
|
|
124
|
+
"public.systems": {
|
|
125
|
+
"name": "systems",
|
|
126
|
+
"schema": "",
|
|
127
|
+
"columns": {
|
|
128
|
+
"id": {
|
|
129
|
+
"name": "id",
|
|
130
|
+
"type": "text",
|
|
131
|
+
"primaryKey": true,
|
|
132
|
+
"notNull": true
|
|
133
|
+
},
|
|
134
|
+
"name": {
|
|
135
|
+
"name": "name",
|
|
136
|
+
"type": "text",
|
|
137
|
+
"primaryKey": false,
|
|
138
|
+
"notNull": true
|
|
139
|
+
},
|
|
140
|
+
"description": {
|
|
141
|
+
"name": "description",
|
|
142
|
+
"type": "text",
|
|
143
|
+
"primaryKey": false,
|
|
144
|
+
"notNull": false
|
|
145
|
+
},
|
|
146
|
+
"metadata": {
|
|
147
|
+
"name": "metadata",
|
|
148
|
+
"type": "json",
|
|
149
|
+
"primaryKey": false,
|
|
150
|
+
"notNull": false,
|
|
151
|
+
"default": "'{}'::json"
|
|
152
|
+
},
|
|
153
|
+
"created_at": {
|
|
154
|
+
"name": "created_at",
|
|
155
|
+
"type": "timestamp",
|
|
156
|
+
"primaryKey": false,
|
|
157
|
+
"notNull": true,
|
|
158
|
+
"default": "now()"
|
|
159
|
+
},
|
|
160
|
+
"updated_at": {
|
|
161
|
+
"name": "updated_at",
|
|
162
|
+
"type": "timestamp",
|
|
163
|
+
"primaryKey": false,
|
|
164
|
+
"notNull": true,
|
|
165
|
+
"default": "now()"
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
"indexes": {},
|
|
169
|
+
"foreignKeys": {},
|
|
170
|
+
"compositePrimaryKeys": {},
|
|
171
|
+
"uniqueConstraints": {},
|
|
172
|
+
"policies": {},
|
|
173
|
+
"checkConstraints": {},
|
|
174
|
+
"isRLSEnabled": false
|
|
175
|
+
},
|
|
176
|
+
"public.systems_groups": {
|
|
177
|
+
"name": "systems_groups",
|
|
178
|
+
"schema": "",
|
|
179
|
+
"columns": {
|
|
180
|
+
"system_id": {
|
|
181
|
+
"name": "system_id",
|
|
182
|
+
"type": "text",
|
|
183
|
+
"primaryKey": false,
|
|
184
|
+
"notNull": true
|
|
185
|
+
},
|
|
186
|
+
"group_id": {
|
|
187
|
+
"name": "group_id",
|
|
188
|
+
"type": "text",
|
|
189
|
+
"primaryKey": false,
|
|
190
|
+
"notNull": true
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
"indexes": {},
|
|
194
|
+
"foreignKeys": {
|
|
195
|
+
"systems_groups_system_id_systems_id_fk": {
|
|
196
|
+
"name": "systems_groups_system_id_systems_id_fk",
|
|
197
|
+
"tableFrom": "systems_groups",
|
|
198
|
+
"tableTo": "systems",
|
|
199
|
+
"columnsFrom": [
|
|
200
|
+
"system_id"
|
|
201
|
+
],
|
|
202
|
+
"columnsTo": [
|
|
203
|
+
"id"
|
|
204
|
+
],
|
|
205
|
+
"onDelete": "cascade",
|
|
206
|
+
"onUpdate": "no action"
|
|
207
|
+
},
|
|
208
|
+
"systems_groups_group_id_groups_id_fk": {
|
|
209
|
+
"name": "systems_groups_group_id_groups_id_fk",
|
|
210
|
+
"tableFrom": "systems_groups",
|
|
211
|
+
"tableTo": "groups",
|
|
212
|
+
"columnsFrom": [
|
|
213
|
+
"group_id"
|
|
214
|
+
],
|
|
215
|
+
"columnsTo": [
|
|
216
|
+
"id"
|
|
217
|
+
],
|
|
218
|
+
"onDelete": "cascade",
|
|
219
|
+
"onUpdate": "no action"
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
"compositePrimaryKeys": {
|
|
223
|
+
"systems_groups_system_id_group_id_pk": {
|
|
224
|
+
"name": "systems_groups_system_id_group_id_pk",
|
|
225
|
+
"columns": [
|
|
226
|
+
"system_id",
|
|
227
|
+
"group_id"
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
"uniqueConstraints": {},
|
|
232
|
+
"policies": {},
|
|
233
|
+
"checkConstraints": {},
|
|
234
|
+
"isRLSEnabled": false
|
|
235
|
+
},
|
|
236
|
+
"public.views": {
|
|
237
|
+
"name": "views",
|
|
238
|
+
"schema": "",
|
|
239
|
+
"columns": {
|
|
240
|
+
"id": {
|
|
241
|
+
"name": "id",
|
|
242
|
+
"type": "text",
|
|
243
|
+
"primaryKey": true,
|
|
244
|
+
"notNull": true
|
|
245
|
+
},
|
|
246
|
+
"name": {
|
|
247
|
+
"name": "name",
|
|
248
|
+
"type": "text",
|
|
249
|
+
"primaryKey": false,
|
|
250
|
+
"notNull": true
|
|
251
|
+
},
|
|
252
|
+
"description": {
|
|
253
|
+
"name": "description",
|
|
254
|
+
"type": "text",
|
|
255
|
+
"primaryKey": false,
|
|
256
|
+
"notNull": false
|
|
257
|
+
},
|
|
258
|
+
"configuration": {
|
|
259
|
+
"name": "configuration",
|
|
260
|
+
"type": "json",
|
|
261
|
+
"primaryKey": false,
|
|
262
|
+
"notNull": true,
|
|
263
|
+
"default": "'[]'::json"
|
|
264
|
+
},
|
|
265
|
+
"created_at": {
|
|
266
|
+
"name": "created_at",
|
|
267
|
+
"type": "timestamp",
|
|
268
|
+
"primaryKey": false,
|
|
269
|
+
"notNull": true,
|
|
270
|
+
"default": "now()"
|
|
271
|
+
},
|
|
272
|
+
"updated_at": {
|
|
273
|
+
"name": "updated_at",
|
|
274
|
+
"type": "timestamp",
|
|
275
|
+
"primaryKey": false,
|
|
276
|
+
"notNull": true,
|
|
277
|
+
"default": "now()"
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
"indexes": {},
|
|
281
|
+
"foreignKeys": {},
|
|
282
|
+
"compositePrimaryKeys": {},
|
|
283
|
+
"uniqueConstraints": {},
|
|
284
|
+
"policies": {},
|
|
285
|
+
"checkConstraints": {},
|
|
286
|
+
"isRLSEnabled": false
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
"enums": {
|
|
290
|
+
"public.contact_type": {
|
|
291
|
+
"name": "contact_type",
|
|
292
|
+
"schema": "public",
|
|
293
|
+
"values": [
|
|
294
|
+
"user",
|
|
295
|
+
"mailbox"
|
|
296
|
+
]
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
"schemas": {},
|
|
300
|
+
"sequences": {},
|
|
301
|
+
"roles": {},
|
|
302
|
+
"policies": {},
|
|
303
|
+
"views": {},
|
|
304
|
+
"_meta": {
|
|
305
|
+
"columns": {},
|
|
306
|
+
"schemas": {},
|
|
307
|
+
"tables": {}
|
|
308
|
+
}
|
|
309
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/catalog-backend",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"scripts": {
|
|
@@ -10,21 +10,23 @@
|
|
|
10
10
|
"lint:code": "eslint . --max-warnings 0"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@checkstack/backend-api": "0.5.
|
|
14
|
-
"@checkstack/
|
|
15
|
-
"@checkstack/
|
|
16
|
-
"@checkstack/
|
|
13
|
+
"@checkstack/backend-api": "0.5.2",
|
|
14
|
+
"@checkstack/auth-common": "0.5.4",
|
|
15
|
+
"@checkstack/catalog-common": "1.2.5",
|
|
16
|
+
"@checkstack/command-backend": "0.1.8",
|
|
17
|
+
"@checkstack/auth-backend": "0.4.5",
|
|
18
|
+
"@checkstack/notification-common": "0.2.4",
|
|
17
19
|
"@orpc/server": "^1.13.2",
|
|
18
20
|
"drizzle-orm": "^0.45.1",
|
|
19
21
|
"hono": "^4.0.0",
|
|
20
22
|
"uuid": "^13.0.0",
|
|
21
23
|
"zod": "^4.2.1",
|
|
22
|
-
"@checkstack/common": "0.6.
|
|
24
|
+
"@checkstack/common": "0.6.1"
|
|
23
25
|
},
|
|
24
26
|
"devDependencies": {
|
|
25
|
-
"@checkstack/drizzle-helper": "0.0.
|
|
26
|
-
"@checkstack/scripts": "0.1.
|
|
27
|
-
"@checkstack/tsconfig": "0.0.
|
|
27
|
+
"@checkstack/drizzle-helper": "0.0.3",
|
|
28
|
+
"@checkstack/scripts": "0.1.1",
|
|
29
|
+
"@checkstack/tsconfig": "0.0.3",
|
|
28
30
|
"@types/bun": "^1.3.5",
|
|
29
31
|
"@types/node": "^20.0.0",
|
|
30
32
|
"@types/uuid": "^11.0.0",
|
package/src/index.ts
CHANGED
|
@@ -12,8 +12,11 @@ import {
|
|
|
12
12
|
} from "@checkstack/catalog-common";
|
|
13
13
|
import { createCatalogRouter } from "./router";
|
|
14
14
|
import { NotificationApi } from "@checkstack/notification-common";
|
|
15
|
+
import { AuthApi } from "@checkstack/auth-common";
|
|
16
|
+
import { authHooks } from "@checkstack/auth-backend";
|
|
15
17
|
import { resolveRoute, type InferClient } from "@checkstack/common";
|
|
16
18
|
import { registerSearchProvider } from "@checkstack/command-backend";
|
|
19
|
+
import { EntityService } from "./services/entity-service";
|
|
17
20
|
|
|
18
21
|
// Database schema is still needed for types in creating the router
|
|
19
22
|
import * as schema from "./schema";
|
|
@@ -43,11 +46,13 @@ export default createBackendPlugin({
|
|
|
43
46
|
|
|
44
47
|
// Get notification client for group management and sending notifications
|
|
45
48
|
const notificationClient = rpcClient.forPlugin(NotificationApi);
|
|
49
|
+
const authClient = rpcClient.forPlugin(AuthApi);
|
|
46
50
|
|
|
47
|
-
// Register oRPC router with notification client
|
|
51
|
+
// Register oRPC router with notification client and auth client
|
|
48
52
|
const catalogRouter = createCatalogRouter({
|
|
49
53
|
database: typedDb,
|
|
50
54
|
notificationClient,
|
|
55
|
+
authClient,
|
|
51
56
|
pluginId: pluginMetadata.pluginId,
|
|
52
57
|
});
|
|
53
58
|
rpc.registerRouter(catalogRouter, catalogContract);
|
|
@@ -108,12 +113,24 @@ export default createBackendPlugin({
|
|
|
108
113
|
logger.debug("✅ Catalog Backend initialized.");
|
|
109
114
|
},
|
|
110
115
|
// Phase 3: Safe to make RPC calls after all plugins are ready
|
|
111
|
-
afterPluginsReady: async ({ database, rpcClient, logger }) => {
|
|
116
|
+
afterPluginsReady: async ({ database, rpcClient, logger, onHook }) => {
|
|
112
117
|
const typedDb = database as SafeDatabase<typeof schema>;
|
|
113
118
|
const notificationClient = rpcClient.forPlugin(NotificationApi);
|
|
114
119
|
|
|
115
120
|
// Bootstrap: Create notification groups for existing systems and groups
|
|
116
121
|
await bootstrapNotificationGroups(typedDb, notificationClient, logger);
|
|
122
|
+
|
|
123
|
+
// Subscribe to user deletion to clean up user contacts
|
|
124
|
+
onHook(
|
|
125
|
+
authHooks.userDeleted,
|
|
126
|
+
async ({ userId }) => {
|
|
127
|
+
logger.debug(`Cleaning up contacts for deleted user: ${userId}`);
|
|
128
|
+
const entityService = new EntityService(typedDb);
|
|
129
|
+
await entityService.deleteContactsByUserId(userId);
|
|
130
|
+
logger.debug(`Cleaned up contacts for user: ${userId}`);
|
|
131
|
+
},
|
|
132
|
+
{ mode: "work-queue", workerGroup: "user-cleanup" },
|
|
133
|
+
);
|
|
117
134
|
},
|
|
118
135
|
});
|
|
119
136
|
},
|
package/src/router.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { implement, ORPCError } from "@orpc/server";
|
|
2
2
|
import { autoAuthMiddleware, type RpcContext } from "@checkstack/backend-api";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
catalogContract,
|
|
5
|
+
type SystemContact,
|
|
6
|
+
} from "@checkstack/catalog-common";
|
|
4
7
|
import { EntityService } from "./services/entity-service";
|
|
5
8
|
import type { SafeDatabase } from "@checkstack/backend-api";
|
|
6
9
|
import * as schema from "./schema";
|
|
7
10
|
import { NotificationApi } from "@checkstack/notification-common";
|
|
11
|
+
import { AuthApi } from "@checkstack/auth-common";
|
|
8
12
|
import type { InferClient } from "@checkstack/common";
|
|
9
13
|
import { catalogHooks } from "./hooks";
|
|
10
14
|
import { eq } from "drizzle-orm";
|
|
@@ -22,12 +26,14 @@ const os = implement(catalogContract)
|
|
|
22
26
|
export interface CatalogRouterDeps {
|
|
23
27
|
database: SafeDatabase<typeof schema>;
|
|
24
28
|
notificationClient: InferClient<typeof NotificationApi>;
|
|
29
|
+
authClient: InferClient<typeof AuthApi>;
|
|
25
30
|
pluginId: string;
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
export const createCatalogRouter = ({
|
|
29
34
|
database,
|
|
30
35
|
notificationClient,
|
|
36
|
+
authClient,
|
|
31
37
|
pluginId,
|
|
32
38
|
}: CatalogRouterDeps) => {
|
|
33
39
|
const entityService = new EntityService(database);
|
|
@@ -36,7 +42,7 @@ export const createCatalogRouter = ({
|
|
|
36
42
|
const createNotificationGroup = async (
|
|
37
43
|
type: "system" | "group",
|
|
38
44
|
id: string,
|
|
39
|
-
name: string
|
|
45
|
+
name: string,
|
|
40
46
|
) => {
|
|
41
47
|
try {
|
|
42
48
|
await notificationClient.createGroup({
|
|
@@ -49,7 +55,7 @@ export const createCatalogRouter = ({
|
|
|
49
55
|
// Log but don't fail the operation
|
|
50
56
|
console.warn(
|
|
51
57
|
`Failed to create notification group for ${type} ${id}:`,
|
|
52
|
-
error
|
|
58
|
+
error,
|
|
53
59
|
);
|
|
54
60
|
}
|
|
55
61
|
};
|
|
@@ -57,7 +63,7 @@ export const createCatalogRouter = ({
|
|
|
57
63
|
// Helper to delete notification group for an entity
|
|
58
64
|
const deleteNotificationGroup = async (
|
|
59
65
|
type: "system" | "group",
|
|
60
|
-
id: string
|
|
66
|
+
id: string,
|
|
61
67
|
) => {
|
|
62
68
|
try {
|
|
63
69
|
await notificationClient.deleteGroup({
|
|
@@ -68,7 +74,7 @@ export const createCatalogRouter = ({
|
|
|
68
74
|
// Log but don't fail the operation
|
|
69
75
|
console.warn(
|
|
70
76
|
`Failed to delete notification group for ${type} ${id}:`,
|
|
71
|
-
error
|
|
77
|
+
error,
|
|
72
78
|
);
|
|
73
79
|
}
|
|
74
80
|
};
|
|
@@ -134,14 +140,11 @@ export const createCatalogRouter = ({
|
|
|
134
140
|
const cleanData: Partial<{
|
|
135
141
|
name: string;
|
|
136
142
|
description?: string;
|
|
137
|
-
owner?: string;
|
|
138
143
|
metadata?: Record<string, unknown>;
|
|
139
144
|
}> = {};
|
|
140
145
|
if (input.data.name !== undefined) cleanData.name = input.data.name;
|
|
141
146
|
if (input.data.description !== undefined)
|
|
142
147
|
cleanData.description = input.data.description ?? undefined;
|
|
143
|
-
if (input.data.owner !== undefined)
|
|
144
|
-
cleanData.owner = input.data.owner ?? undefined;
|
|
145
148
|
if (input.data.metadata !== undefined)
|
|
146
149
|
cleanData.metadata = input.data.metadata ?? undefined;
|
|
147
150
|
|
|
@@ -231,7 +234,7 @@ export const createCatalogRouter = ({
|
|
|
231
234
|
async ({ input }) => {
|
|
232
235
|
await entityService.removeSystemFromGroup(input);
|
|
233
236
|
return { success: true };
|
|
234
|
-
}
|
|
237
|
+
},
|
|
235
238
|
);
|
|
236
239
|
|
|
237
240
|
const getViews = os.getViews.handler(async () => entityService.getViews());
|
|
@@ -244,6 +247,97 @@ export const createCatalogRouter = ({
|
|
|
244
247
|
});
|
|
245
248
|
});
|
|
246
249
|
|
|
250
|
+
// System Contacts handlers
|
|
251
|
+
const getSystemContacts = os.getSystemContacts.handler(async ({ input }) => {
|
|
252
|
+
const rawContacts = await entityService.getContactsForSystem(
|
|
253
|
+
input.systemId,
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// Resolve user profiles for user-type contacts
|
|
257
|
+
const enrichedContacts: SystemContact[] = await Promise.all(
|
|
258
|
+
rawContacts.map(async (contact) => {
|
|
259
|
+
if (contact.type === "user" && contact.userId) {
|
|
260
|
+
// Resolve user profile via auth service
|
|
261
|
+
const user = await authClient.getUserById({ userId: contact.userId });
|
|
262
|
+
return {
|
|
263
|
+
id: contact.id,
|
|
264
|
+
systemId: contact.systemId,
|
|
265
|
+
type: "user" as const,
|
|
266
|
+
userId: contact.userId,
|
|
267
|
+
label: contact.label,
|
|
268
|
+
userName: user?.name ?? undefined,
|
|
269
|
+
userEmail: user?.email ?? undefined,
|
|
270
|
+
createdAt: contact.createdAt,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
// Mailbox contact
|
|
274
|
+
return {
|
|
275
|
+
id: contact.id,
|
|
276
|
+
systemId: contact.systemId,
|
|
277
|
+
type: "mailbox" as const,
|
|
278
|
+
email: contact.email ?? "",
|
|
279
|
+
label: contact.label,
|
|
280
|
+
createdAt: contact.createdAt,
|
|
281
|
+
};
|
|
282
|
+
}),
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
return enrichedContacts;
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const addSystemContact = os.addSystemContact.handler(async ({ input }) => {
|
|
289
|
+
// Validate input based on type
|
|
290
|
+
if (input.type === "user" && !input.userId) {
|
|
291
|
+
throw new ORPCError("BAD_REQUEST", {
|
|
292
|
+
message: "userId is required for user-type contacts",
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
if (input.type === "mailbox" && !input.email) {
|
|
296
|
+
throw new ORPCError("BAD_REQUEST", {
|
|
297
|
+
message: "email is required for mailbox-type contacts",
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const result = await entityService.addContact({
|
|
302
|
+
systemId: input.systemId,
|
|
303
|
+
type: input.type,
|
|
304
|
+
userId: input.type === "user" ? input.userId : undefined,
|
|
305
|
+
email: input.type === "mailbox" ? input.email : undefined,
|
|
306
|
+
label: input.label,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Return the enriched contact
|
|
310
|
+
if (result.type === "user" && result.userId) {
|
|
311
|
+
const user = await authClient.getUserById({ userId: result.userId });
|
|
312
|
+
return {
|
|
313
|
+
id: result.id,
|
|
314
|
+
systemId: result.systemId,
|
|
315
|
+
type: "user" as const,
|
|
316
|
+
userId: result.userId,
|
|
317
|
+
label: result.label,
|
|
318
|
+
userName: user?.name ?? undefined,
|
|
319
|
+
userEmail: user?.email ?? undefined,
|
|
320
|
+
createdAt: result.createdAt,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
id: result.id,
|
|
326
|
+
systemId: result.systemId,
|
|
327
|
+
type: "mailbox" as const,
|
|
328
|
+
email: result.email ?? "",
|
|
329
|
+
label: result.label,
|
|
330
|
+
createdAt: result.createdAt,
|
|
331
|
+
};
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const removeSystemContact = os.removeSystemContact.handler(
|
|
335
|
+
async ({ input }) => {
|
|
336
|
+
await entityService.removeContact(input);
|
|
337
|
+
return { success: true };
|
|
338
|
+
},
|
|
339
|
+
);
|
|
340
|
+
|
|
247
341
|
/**
|
|
248
342
|
* Notify all users subscribed to a system (and optionally its groups).
|
|
249
343
|
* Delegates deduplication to notification-backend via notifyGroups RPC.
|
|
@@ -272,7 +366,7 @@ export const createCatalogRouter = ({
|
|
|
272
366
|
|
|
273
367
|
// Spread to avoid mutation
|
|
274
368
|
groupIds.push(
|
|
275
|
-
...systemGroups.map(({ groupId }) => `${pluginId}.group.${groupId}`)
|
|
369
|
+
...systemGroups.map(({ groupId }) => `${pluginId}.group.${groupId}`),
|
|
276
370
|
);
|
|
277
371
|
}
|
|
278
372
|
|
|
@@ -286,7 +380,7 @@ export const createCatalogRouter = ({
|
|
|
286
380
|
});
|
|
287
381
|
|
|
288
382
|
return { notifiedCount: result.notifiedCount };
|
|
289
|
-
}
|
|
383
|
+
},
|
|
290
384
|
);
|
|
291
385
|
|
|
292
386
|
// Build and return the router
|
|
@@ -298,6 +392,9 @@ export const createCatalogRouter = ({
|
|
|
298
392
|
createSystem,
|
|
299
393
|
updateSystem,
|
|
300
394
|
deleteSystem,
|
|
395
|
+
getSystemContacts,
|
|
396
|
+
addSystemContact,
|
|
397
|
+
removeSystemContact,
|
|
301
398
|
createGroup,
|
|
302
399
|
updateGroup,
|
|
303
400
|
deleteGroup,
|
package/src/schema.ts
CHANGED
|
@@ -1,22 +1,40 @@
|
|
|
1
1
|
import {
|
|
2
2
|
pgTable,
|
|
3
|
+
pgEnum,
|
|
3
4
|
text,
|
|
4
5
|
timestamp,
|
|
5
6
|
json,
|
|
6
7
|
primaryKey,
|
|
7
8
|
} from "drizzle-orm/pg-core";
|
|
8
9
|
|
|
10
|
+
// Enums
|
|
11
|
+
export const contactTypeEnum = pgEnum("contact_type", ["user", "mailbox"]);
|
|
12
|
+
|
|
9
13
|
// Tables use pgTable (schemaless) - runtime schema is set via search_path
|
|
10
14
|
export const systems = pgTable("systems", {
|
|
11
15
|
id: text("id").primaryKey(),
|
|
12
16
|
name: text("name").notNull(),
|
|
13
17
|
description: text("description"),
|
|
14
|
-
owner: text("owner"), // user_id or group_id reference? Keeping as text for now.
|
|
15
18
|
metadata: json("metadata").default({}),
|
|
16
19
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
17
20
|
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
18
21
|
});
|
|
19
22
|
|
|
23
|
+
export const systemContacts = pgTable("system_contacts", {
|
|
24
|
+
id: text("id").primaryKey(),
|
|
25
|
+
systemId: text("system_id")
|
|
26
|
+
.notNull()
|
|
27
|
+
.references(() => systems.id, { onDelete: "cascade" }),
|
|
28
|
+
type: contactTypeEnum("type").notNull(),
|
|
29
|
+
// For type="user": userId references auth user
|
|
30
|
+
userId: text("user_id"),
|
|
31
|
+
// For type="mailbox": store email directly
|
|
32
|
+
email: text("email"),
|
|
33
|
+
// Optional label for display (e.g., "On-Call", "Team Lead")
|
|
34
|
+
label: text("label"),
|
|
35
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
36
|
+
});
|
|
37
|
+
|
|
20
38
|
export const groups = pgTable("groups", {
|
|
21
39
|
id: text("id").primaryKey(),
|
|
22
40
|
name: text("name").notNull(),
|
|
@@ -38,7 +56,7 @@ export const systemsGroups = pgTable(
|
|
|
38
56
|
},
|
|
39
57
|
(t) => ({
|
|
40
58
|
pk: primaryKey(t.systemId, t.groupId),
|
|
41
|
-
})
|
|
59
|
+
}),
|
|
42
60
|
);
|
|
43
61
|
|
|
44
62
|
export const views = pgTable("views", {
|
|
@@ -37,7 +37,6 @@ describe("EntityService", () => {
|
|
|
37
37
|
const fullSystem = {
|
|
38
38
|
...data,
|
|
39
39
|
description: null,
|
|
40
|
-
owner: null,
|
|
41
40
|
status: "healthy" as "healthy" | "degraded" | "unhealthy",
|
|
42
41
|
metadata: {},
|
|
43
42
|
createdAt: new Date(),
|
|
@@ -60,7 +59,6 @@ describe("EntityService", () => {
|
|
|
60
59
|
id: "test",
|
|
61
60
|
name: "Updated",
|
|
62
61
|
description: null,
|
|
63
|
-
owner: null,
|
|
64
62
|
status: "healthy" as "healthy" | "degraded" | "unhealthy",
|
|
65
63
|
metadata: {},
|
|
66
64
|
createdAt: new Date(),
|
|
@@ -7,10 +7,17 @@ import { v4 as uuidv4 } from "uuid";
|
|
|
7
7
|
type NewSystem = {
|
|
8
8
|
name: string;
|
|
9
9
|
description?: string;
|
|
10
|
-
owner?: string;
|
|
11
10
|
metadata?: Record<string, unknown>;
|
|
12
11
|
};
|
|
13
12
|
|
|
13
|
+
type NewContact = {
|
|
14
|
+
systemId: string;
|
|
15
|
+
type: "user" | "mailbox";
|
|
16
|
+
userId?: string;
|
|
17
|
+
email?: string;
|
|
18
|
+
label?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
14
21
|
type NewGroup = {
|
|
15
22
|
name: string;
|
|
16
23
|
metadata?: Record<string, unknown>;
|
|
@@ -63,6 +70,34 @@ export class EntityService {
|
|
|
63
70
|
await this.database.delete(schema.systems).where(eq(schema.systems.id, id));
|
|
64
71
|
}
|
|
65
72
|
|
|
73
|
+
// System Contacts
|
|
74
|
+
async getContactsForSystem(systemId: string) {
|
|
75
|
+
return this.database
|
|
76
|
+
.select()
|
|
77
|
+
.from(schema.systemContacts)
|
|
78
|
+
.where(eq(schema.systemContacts.systemId, systemId));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async addContact(data: NewContact) {
|
|
82
|
+
const result = await this.database
|
|
83
|
+
.insert(schema.systemContacts)
|
|
84
|
+
.values({ id: uuidv4(), ...data })
|
|
85
|
+
.returning();
|
|
86
|
+
return result[0];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async removeContact(contactId: string) {
|
|
90
|
+
await this.database
|
|
91
|
+
.delete(schema.systemContacts)
|
|
92
|
+
.where(eq(schema.systemContacts.id, contactId));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async deleteContactsByUserId(userId: string) {
|
|
96
|
+
await this.database
|
|
97
|
+
.delete(schema.systemContacts)
|
|
98
|
+
.where(eq(schema.systemContacts.userId, userId));
|
|
99
|
+
}
|
|
100
|
+
|
|
66
101
|
// Groups
|
|
67
102
|
async getGroups() {
|
|
68
103
|
// Fetch all groups
|
|
@@ -124,8 +159,8 @@ export class EntityService {
|
|
|
124
159
|
.where(
|
|
125
160
|
and(
|
|
126
161
|
eq(schema.systemsGroups.groupId, groupId),
|
|
127
|
-
eq(schema.systemsGroups.systemId, systemId)
|
|
128
|
-
)
|
|
162
|
+
eq(schema.systemsGroups.systemId, systemId),
|
|
163
|
+
),
|
|
129
164
|
);
|
|
130
165
|
}
|
|
131
166
|
|