@checkstack/catalog-backend 0.3.0 → 0.4.1

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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # @checkstack/catalog-backend
2
2
 
3
+ ## 0.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [86bab6a]
8
+ - @checkstack/gitops-backend@0.1.1
9
+ - @checkstack/gitops-common@0.1.1
10
+
11
+ ## 0.4.0
12
+
13
+ ### Minor Changes
14
+
15
+ - b01078f: Added GitOps System kind extension for managing system group associations
16
+
3
17
  ## 0.3.0
4
18
 
5
19
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/catalog-backend",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "checkstack": {
@@ -18,8 +18,8 @@
18
18
  "@checkstack/catalog-common": "1.3.1",
19
19
  "@checkstack/command-backend": "0.1.19",
20
20
  "@checkstack/auth-backend": "0.4.18",
21
- "@checkstack/gitops-backend": "0.0.1",
22
- "@checkstack/gitops-common": "0.0.1",
21
+ "@checkstack/gitops-backend": "0.1.0",
22
+ "@checkstack/gitops-common": "0.1.0",
23
23
  "@checkstack/notification-common": "0.2.8",
24
24
  "@orpc/server": "^1.13.2",
25
25
  "drizzle-orm": "^0.45.0",
@@ -85,6 +85,11 @@ function createMockEntityService() {
85
85
  const idx = groups.findIndex((g) => g.id === id);
86
86
  if (idx >= 0) groups.splice(idx, 1);
87
87
  }),
88
+ addSystemToGroup: mock(async (props: { groupId: string; systemId: string }) => {}),
89
+ removeSystemFromGroup: mock(async (props: { groupId: string; systemId: string }) => {}),
90
+ getGroupsForSystem: mock(async (systemId: string) => {
91
+ return [] as { groupId: string, systemId: string }[];
92
+ }),
88
93
  };
89
94
  }
90
95
 
@@ -271,6 +276,74 @@ describe("Catalog GitOps Kind: System", () => {
271
276
  });
272
277
  });
273
278
 
279
+ describe("Catalog GitOps Kind Extension: System -> groups", () => {
280
+ let mockService: ReturnType<typeof createMockEntityService>;
281
+
282
+ beforeEach(() => {
283
+ mockService = createMockEntityService();
284
+ });
285
+
286
+ it("associates system with groups and removes stale ones", async () => {
287
+ // We mock getGroupsForSystem to return one existing association
288
+ mockService.getGroupsForSystem.mockResolvedValueOnce([
289
+ { groupId: "grp-stale", systemId: "sys-1" }
290
+ ]);
291
+
292
+ const mockExtContext: ReconcileContext = {
293
+ ...mockContext,
294
+ resolveEntityRef: mock(async ({ entityName }) => {
295
+ if (entityName === "new-group") return "grp-new";
296
+ return undefined;
297
+ }),
298
+ };
299
+
300
+ // Simulate the inline reconcile logic from index.ts
301
+ const reconcileExt = async ({ extensionSpec, entityId, context }: any) => {
302
+ if (!extensionSpec || extensionSpec.length === 0) return;
303
+
304
+ const desiredGroupIds = new Set<string>();
305
+
306
+ for (const entry of extensionSpec) {
307
+ const groupId = await context.resolveEntityRef({
308
+ kind: entry.kind,
309
+ entityName: entry.name,
310
+ });
311
+ if (groupId) {
312
+ desiredGroupIds.add(groupId);
313
+ await mockService.addSystemToGroup({ groupId, systemId: entityId });
314
+ }
315
+ }
316
+
317
+ const currentAssociations = await mockService.getGroupsForSystem(entityId);
318
+ for (const existing of currentAssociations) {
319
+ if (!desiredGroupIds.has(existing.groupId)) {
320
+ await mockService.removeSystemFromGroup({
321
+ groupId: existing.groupId,
322
+ systemId: entityId,
323
+ });
324
+ }
325
+ }
326
+ };
327
+
328
+ await reconcileExt({
329
+ entity: { metadata: { name: "my-system" } },
330
+ extensionSpec: [{ kind: "Group", name: "new-group" }],
331
+ entityId: "sys-1",
332
+ context: mockExtContext,
333
+ });
334
+
335
+ expect(mockService.addSystemToGroup).toHaveBeenCalledWith({
336
+ groupId: "grp-new",
337
+ systemId: "sys-1",
338
+ });
339
+
340
+ expect(mockService.removeSystemFromGroup).toHaveBeenCalledWith({
341
+ groupId: "grp-stale",
342
+ systemId: "sys-1",
343
+ });
344
+ });
345
+ });
346
+
274
347
  describe("Catalog GitOps Kind: Group", () => {
275
348
  let mockService: ReturnType<typeof createMockEntityService>;
276
349
 
package/src/index.ts CHANGED
@@ -18,7 +18,7 @@ import { resolveRoute, type InferClient, extractErrorMessage} from "@checkstack/
18
18
  import { registerSearchProvider } from "@checkstack/command-backend";
19
19
  import { EntityService } from "./services/entity-service";
20
20
  import { entityKindExtensionPoint } from "@checkstack/gitops-backend";
21
- import { CHECKSTACK_API_VERSION } from "@checkstack/gitops-common";
21
+ import { CHECKSTACK_API_VERSION, entityRefSchema } from "@checkstack/gitops-common";
22
22
  import { z } from "zod";
23
23
 
24
24
  // Database schema is still needed for types in creating the router
@@ -82,6 +82,61 @@ export default createBackendPlugin({
82
82
  },
83
83
  });
84
84
 
85
+ // Register kind extension: System -> groups
86
+ kindRegistry.registerKindExtension({
87
+ apiVersion: CHECKSTACK_API_VERSION,
88
+ kind: "System",
89
+ namespace: "groups",
90
+ specSchema: z.array(entityRefSchema).optional(),
91
+ reconcile: async ({ entity, extensionSpec, entityId, context }) => {
92
+ if (!gitopsDb) throw new Error("Catalog database not initialized");
93
+ if (!extensionSpec || extensionSpec.length === 0) return;
94
+
95
+ const entityService = new EntityService(gitopsDb);
96
+ const systemEntityId = entityId;
97
+
98
+ const desiredGroupIds = new Set<string>();
99
+
100
+ for (const entry of extensionSpec) {
101
+ const groupId = await context.resolveEntityRef({
102
+ kind: entry.kind,
103
+ entityName: entry.name,
104
+ });
105
+
106
+ if (!groupId) {
107
+ throw new Error(
108
+ `Cannot resolve ${entry.kind} ref "${entry.name}" — ensure the entity exists`
109
+ );
110
+ }
111
+
112
+ desiredGroupIds.add(groupId);
113
+
114
+ await entityService.addSystemToGroup({
115
+ groupId: groupId,
116
+ systemId: systemEntityId,
117
+ });
118
+
119
+ context.logger.info(
120
+ `GitOps: associated System "${entity.metadata.name}" with Group "${entry.name}" (${groupId})`
121
+ );
122
+ }
123
+
124
+ // Remove stale associations not in the spec
125
+ const currentAssociations = await entityService.getGroupsForSystem(systemEntityId);
126
+ for (const existing of currentAssociations) {
127
+ if (!desiredGroupIds.has(existing.groupId)) {
128
+ await entityService.removeSystemFromGroup({
129
+ groupId: existing.groupId,
130
+ systemId: systemEntityId,
131
+ });
132
+ context.logger.info(
133
+ `GitOps: removed stale association ${existing.groupId} from System "${entity.metadata.name}"`
134
+ );
135
+ }
136
+ }
137
+ },
138
+ });
139
+
85
140
  // Register kind: Group
86
141
  kindRegistry.registerKind({
87
142
  apiVersion: CHECKSTACK_API_VERSION,
@@ -144,6 +144,15 @@ export class EntityService {
144
144
  await this.database.delete(schema.groups).where(eq(schema.groups.id, id));
145
145
  }
146
146
 
147
+ async getGroupsForSystem(systemId: string) {
148
+ const associations = await this.database
149
+ .select()
150
+ .from(schema.systemsGroups)
151
+ .where(eq(schema.systemsGroups.systemId, systemId));
152
+
153
+ return associations;
154
+ }
155
+
147
156
  async addSystemToGroup(props: { groupId: string; systemId: string }) {
148
157
  const { groupId, systemId } = props;
149
158
  await this.database