@checkstack/catalog-backend 1.3.1 → 1.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/CHANGELOG.md +136 -0
- package/drizzle/0003_tan_spot.sql +17 -0
- package/drizzle/0004_heavy_sharon_carter.sql +13 -0
- package/drizzle/0005_normal_shaman.sql +60 -0
- package/drizzle/0006_optimal_gamora.sql +43 -0
- package/drizzle/meta/0003_snapshot.json +479 -0
- package/drizzle/meta/0004_snapshot.json +495 -0
- package/drizzle/meta/0005_snapshot.json +592 -0
- package/drizzle/meta/0006_snapshot.json +592 -0
- package/drizzle/meta/_journal.json +28 -0
- package/package.json +15 -12
- package/src/ai/catalog-add-system-to-group.test.ts +51 -0
- package/src/ai/catalog-add-system-to-group.ts +68 -0
- package/src/ai/catalog-create-group.test.ts +62 -0
- package/src/ai/catalog-create-group.ts +71 -0
- package/src/ai/catalog-create-system.test.ts +62 -0
- package/src/ai/catalog-create-system.ts +78 -0
- package/src/ai/catalog-delete-group.test.ts +83 -0
- package/src/ai/catalog-delete-group.ts +77 -0
- package/src/ai/catalog-delete-system.test.ts +84 -0
- package/src/ai/catalog-delete-system.ts +77 -0
- package/src/ai/catalog-remove-system-from-group.test.ts +55 -0
- package/src/ai/catalog-remove-system-from-group.ts +74 -0
- package/src/ai/catalog-update-group.test.ts +85 -0
- package/src/ai/catalog-update-group.ts +88 -0
- package/src/ai/catalog-update-system.test.ts +87 -0
- package/src/ai/catalog-update-system.ts +93 -0
- package/src/ai/catalog.projection.test.ts +37 -0
- package/src/ai/register-ai-tools.ts +35 -0
- package/src/automations.test.ts +2 -1
- package/src/catalog-gitops-kinds.test.ts +288 -0
- package/src/index.ts +149 -0
- package/src/router.test.ts +107 -0
- package/src/router.ts +200 -26
- package/src/schema.ts +124 -38
- package/src/services/entity-service.test.ts +28 -0
- package/src/services/entity-service.ts +154 -1
- package/src/services/environment-membership.test.ts +66 -0
- package/src/services/environment-membership.ts +40 -0
- package/src/services/pg-errors.test.ts +24 -0
- package/src/services/pg-errors.ts +21 -0
- package/tsconfig.json +6 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { diffSystemEnvironments } from "./environment-membership";
|
|
3
|
+
|
|
4
|
+
describe("diffSystemEnvironments", () => {
|
|
5
|
+
it("adds desired ids that are not currently linked", () => {
|
|
6
|
+
const diff = diffSystemEnvironments({
|
|
7
|
+
current: ["a"],
|
|
8
|
+
desired: ["a", "b", "c"],
|
|
9
|
+
});
|
|
10
|
+
expect(diff.toAdd.sort()).toEqual(["b", "c"]);
|
|
11
|
+
expect(diff.toRemove).toEqual([]);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("removes current ids no longer desired", () => {
|
|
15
|
+
const diff = diffSystemEnvironments({
|
|
16
|
+
current: ["a", "b", "c"],
|
|
17
|
+
desired: ["a"],
|
|
18
|
+
});
|
|
19
|
+
expect(diff.toAdd).toEqual([]);
|
|
20
|
+
expect(diff.toRemove.sort()).toEqual(["b", "c"]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("computes add and remove together", () => {
|
|
24
|
+
const diff = diffSystemEnvironments({
|
|
25
|
+
current: ["a", "b"],
|
|
26
|
+
desired: ["b", "c"],
|
|
27
|
+
});
|
|
28
|
+
expect(diff.toAdd).toEqual(["c"]);
|
|
29
|
+
expect(diff.toRemove).toEqual(["a"]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("is a no-op when the sets match (order-independent)", () => {
|
|
33
|
+
const diff = diffSystemEnvironments({
|
|
34
|
+
current: ["a", "b"],
|
|
35
|
+
desired: ["b", "a"],
|
|
36
|
+
});
|
|
37
|
+
expect(diff.toAdd).toEqual([]);
|
|
38
|
+
expect(diff.toRemove).toEqual([]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("clears all membership when desired is empty", () => {
|
|
42
|
+
const diff = diffSystemEnvironments({
|
|
43
|
+
current: ["a", "b"],
|
|
44
|
+
desired: [],
|
|
45
|
+
});
|
|
46
|
+
expect(diff.toAdd).toEqual([]);
|
|
47
|
+
expect(diff.toRemove.sort()).toEqual(["a", "b"]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("adds all when current is empty", () => {
|
|
51
|
+
const diff = diffSystemEnvironments({
|
|
52
|
+
current: [],
|
|
53
|
+
desired: ["a", "b"],
|
|
54
|
+
});
|
|
55
|
+
expect(diff.toAdd.sort()).toEqual(["a", "b"]);
|
|
56
|
+
expect(diff.toRemove).toEqual([]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("de-duplicates the desired set", () => {
|
|
60
|
+
const diff = diffSystemEnvironments({
|
|
61
|
+
current: [],
|
|
62
|
+
desired: ["a", "a", "b"],
|
|
63
|
+
});
|
|
64
|
+
expect(diff.toAdd.sort()).toEqual(["a", "b"]);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure desired-set membership diff for systems <-> environments.
|
|
3
|
+
*
|
|
4
|
+
* Both the `setSystemEnvironments` RPC and the GitOps
|
|
5
|
+
* `System -> environments` reconcile compute the same add/prune diff
|
|
6
|
+
* against current membership. Keeping it pure (no DB) makes it
|
|
7
|
+
* straightforward to unit-test without a database harness.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface MembershipDiff {
|
|
11
|
+
/** Environment ids to associate that are not currently linked. */
|
|
12
|
+
toAdd: string[];
|
|
13
|
+
/** Currently-linked environment ids no longer in the desired set. */
|
|
14
|
+
toRemove: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Diff a desired environment-id set against the current set.
|
|
19
|
+
* Order-independent; de-duplicates the desired set.
|
|
20
|
+
*/
|
|
21
|
+
export function diffSystemEnvironments(props: {
|
|
22
|
+
current: ReadonlyArray<string>;
|
|
23
|
+
desired: ReadonlyArray<string>;
|
|
24
|
+
}): MembershipDiff {
|
|
25
|
+
const { current, desired } = props;
|
|
26
|
+
const currentSet = new Set(current);
|
|
27
|
+
const desiredSet = new Set(desired);
|
|
28
|
+
|
|
29
|
+
const toAdd: string[] = [];
|
|
30
|
+
for (const id of desiredSet) {
|
|
31
|
+
if (!currentSet.has(id)) toAdd.push(id);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const toRemove: string[] = [];
|
|
35
|
+
for (const id of currentSet) {
|
|
36
|
+
if (!desiredSet.has(id)) toRemove.push(id);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { toAdd, toRemove };
|
|
40
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { isUniqueViolation } from "./pg-errors";
|
|
3
|
+
|
|
4
|
+
describe("isUniqueViolation", () => {
|
|
5
|
+
it("detects a direct unique violation (code 23505)", () => {
|
|
6
|
+
expect(isUniqueViolation({ code: "23505" })).toBe(true);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("detects a wrapped unique violation (on cause)", () => {
|
|
10
|
+
expect(isUniqueViolation({ cause: { code: "23505" } })).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("ignores other Postgres errors (e.g. FK violation 23503)", () => {
|
|
14
|
+
expect(isUniqueViolation({ code: "23503" })).toBe(false);
|
|
15
|
+
expect(isUniqueViolation({ cause: { code: "23503" } })).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("ignores non-pg-shaped errors", () => {
|
|
19
|
+
expect(isUniqueViolation(null)).toBe(false);
|
|
20
|
+
expect(isUniqueViolation("23505")).toBe(false);
|
|
21
|
+
expect(isUniqueViolation(new Error("boom"))).toBe(false);
|
|
22
|
+
expect(isUniqueViolation({})).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/** Postgres SQLSTATE for a unique-constraint violation. */
|
|
4
|
+
const UNIQUE_VIOLATION = "23505";
|
|
5
|
+
|
|
6
|
+
// A Postgres driver error carries a SQLSTATE string in `code`. Drizzle may
|
|
7
|
+
// rethrow it wrapped, exposing the original on `cause`, so we check both shapes.
|
|
8
|
+
const pgErrorSchema = z.object({ code: z.string() });
|
|
9
|
+
const wrappedPgErrorSchema = z.object({ cause: pgErrorSchema });
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* True when `error` is (or wraps) a Postgres unique-constraint violation. Used
|
|
13
|
+
* to turn a race-induced duplicate insert into a clean `CONFLICT` instead of a
|
|
14
|
+
* 500, mirroring the pre-insert name check.
|
|
15
|
+
*/
|
|
16
|
+
export function isUniqueViolation(error: unknown): boolean {
|
|
17
|
+
const direct = pgErrorSchema.safeParse(error);
|
|
18
|
+
if (direct.success && direct.data.code === UNIQUE_VIOLATION) return true;
|
|
19
|
+
const wrapped = wrappedPgErrorSchema.safeParse(error);
|
|
20
|
+
return wrapped.success && wrapped.data.cause.code === UNIQUE_VIOLATION;
|
|
21
|
+
}
|