@mettlecast/domain-cli 0.2.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 +18 -0
- package/README.md +133 -0
- package/dist/builder/build-registry.d.ts +20 -0
- package/dist/builder/build-registry.js +178 -0
- package/dist/builder/build-types.d.ts +10 -0
- package/dist/builder/build-types.js +32 -0
- package/dist/builder/load-domain-module-config.d.ts +12 -0
- package/dist/builder/load-domain-module-config.js +34 -0
- package/dist/builder/load-module.d.ts +26 -0
- package/dist/builder/load-module.js +114 -0
- package/dist/builder/zod-to-json.d.ts +10 -0
- package/dist/builder/zod-to-json.js +15 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +338 -0
- package/dist/commands/add-api.d.ts +32 -0
- package/dist/commands/add-api.js +106 -0
- package/dist/commands/add-component.d.ts +15 -0
- package/dist/commands/add-component.js +78 -0
- package/dist/commands/add-domain.d.ts +21 -0
- package/dist/commands/add-domain.js +71 -0
- package/dist/commands/add-flow.d.ts +14 -0
- package/dist/commands/add-flow.js +108 -0
- package/dist/commands/add-hook.d.ts +15 -0
- package/dist/commands/add-hook.js +65 -0
- package/dist/commands/add-migration.d.ts +26 -0
- package/dist/commands/add-migration.js +93 -0
- package/dist/commands/add-module.d.ts +7 -0
- package/dist/commands/add-module.js +261 -0
- package/dist/commands/add-page.d.ts +13 -0
- package/dist/commands/add-page.js +170 -0
- package/dist/commands/add-seed-page.d.ts +5 -0
- package/dist/commands/add-seed-page.js +251 -0
- package/dist/commands/add-subscriber.d.ts +30 -0
- package/dist/commands/add-subscriber.js +87 -0
- package/dist/commands/add-test.d.ts +22 -0
- package/dist/commands/add-test.js +285 -0
- package/dist/commands/audit.d.ts +39 -0
- package/dist/commands/audit.js +171 -0
- package/dist/commands/build-catalog.d.ts +68 -0
- package/dist/commands/build-catalog.js +105 -0
- package/dist/commands/build-flows.d.ts +26 -0
- package/dist/commands/build-flows.js +174 -0
- package/dist/commands/build.d.ts +17 -0
- package/dist/commands/build.js +39 -0
- package/dist/commands/check-hashes.d.ts +38 -0
- package/dist/commands/check-hashes.js +97 -0
- package/dist/commands/create-project.d.ts +5 -0
- package/dist/commands/create-project.js +272 -0
- package/dist/commands/dev.d.ts +15 -0
- package/dist/commands/dev.js +27 -0
- package/dist/commands/doctor.d.ts +48 -0
- package/dist/commands/doctor.js +1301 -0
- package/dist/commands/explain.d.ts +34 -0
- package/dist/commands/explain.js +167 -0
- package/dist/commands/power-tune.d.ts +1 -0
- package/dist/commands/power-tune.js +87 -0
- package/dist/commands/reseed-page.d.ts +6 -0
- package/dist/commands/reseed-page.js +297 -0
- package/dist/commands/show-dns.d.ts +15 -0
- package/dist/commands/show-dns.js +101 -0
- package/dist/commands/show.d.ts +46 -0
- package/dist/commands/show.js +135 -0
- package/dist/commands/test.d.ts +18 -0
- package/dist/commands/test.js +56 -0
- package/dist/commands/upgrade-backend.d.ts +9 -0
- package/dist/commands/upgrade-backend.js +46 -0
- package/dist/commands/upgrade-event.d.ts +22 -0
- package/dist/commands/upgrade-event.js +111 -0
- package/dist/commands/upgrade-frontend.d.ts +5 -0
- package/dist/commands/upgrade-frontend.js +99 -0
- package/dist/commands/upgrade.d.ts +7 -0
- package/dist/commands/upgrade.js +495 -0
- package/dist/commands/validate.d.ts +30 -0
- package/dist/commands/validate.js +148 -0
- package/dist/runtime-stubs/hydrate-local-ctx.d.ts +49 -0
- package/dist/runtime-stubs/hydrate-local-ctx.js +45 -0
- package/dist/runtime-stubs/load-secrets.d.ts +11 -0
- package/dist/runtime-stubs/load-secrets.js +37 -0
- package/dist/server/api-server.d.ts +21 -0
- package/dist/server/api-server.js +14 -0
- package/dist/server/mount-routes.d.ts +19 -0
- package/dist/server/mount-routes.js +42 -0
- package/dist/templates/api-skeleton.d.ts +18 -0
- package/dist/templates/api-skeleton.js +69 -0
- package/dist/templates/claude-md.d.ts +4 -0
- package/dist/templates/claude-md.js +7 -0
- package/dist/templates/dashboard-pages/index.d.ts +101 -0
- package/dist/templates/dashboard-pages/index.js +22 -0
- package/dist/templates/domain-config.d.ts +10 -0
- package/dist/templates/domain-config.js +19 -0
- package/dist/templates/events-skeleton.d.ts +9 -0
- package/dist/templates/events-skeleton.js +23 -0
- package/dist/templates/patterns/api/create-with-event.d.ts +5 -0
- package/dist/templates/patterns/api/create-with-event.js +69 -0
- package/dist/templates/patterns/api/idempotent-mutation.d.ts +5 -0
- package/dist/templates/patterns/api/idempotent-mutation.js +99 -0
- package/dist/templates/patterns/api/paginated-list.d.ts +5 -0
- package/dist/templates/patterns/api/paginated-list.js +71 -0
- package/dist/templates/patterns/api/simple-crud.d.ts +6 -0
- package/dist/templates/patterns/api/simple-crud.js +95 -0
- package/dist/templates/patterns/api/system-admin.d.ts +5 -0
- package/dist/templates/patterns/api/system-admin.js +98 -0
- package/dist/templates/patterns/api/webhook-receiver-style.d.ts +5 -0
- package/dist/templates/patterns/api/webhook-receiver-style.js +125 -0
- package/dist/templates/patterns/component/index.d.ts +25 -0
- package/dist/templates/patterns/component/index.js +814 -0
- package/dist/templates/patterns/flow/autonomous-remediation.d.ts +5 -0
- package/dist/templates/patterns/flow/autonomous-remediation.js +98 -0
- package/dist/templates/patterns/flow/fan-out-reaction.d.ts +5 -0
- package/dist/templates/patterns/flow/fan-out-reaction.js +53 -0
- package/dist/templates/patterns/flow/human-in-loop.d.ts +5 -0
- package/dist/templates/patterns/flow/human-in-loop.js +90 -0
- package/dist/templates/patterns/flow/multi-step-orchestration.d.ts +5 -0
- package/dist/templates/patterns/flow/multi-step-orchestration.js +67 -0
- package/dist/templates/patterns/hook/index.d.ts +7 -0
- package/dist/templates/patterns/hook/index.js +378 -0
- package/dist/templates/patterns/migration/column-add.d.ts +5 -0
- package/dist/templates/patterns/migration/column-add.js +52 -0
- package/dist/templates/patterns/migration/event-store.d.ts +6 -0
- package/dist/templates/patterns/migration/event-store.js +97 -0
- package/dist/templates/patterns/migration/index.d.ts +5 -0
- package/dist/templates/patterns/migration/index.js +52 -0
- package/dist/templates/patterns/migration/rls-policy.d.ts +5 -0
- package/dist/templates/patterns/migration/rls-policy.js +78 -0
- package/dist/templates/patterns/migration/tenant-scoped-table.d.ts +6 -0
- package/dist/templates/patterns/migration/tenant-scoped-table.js +81 -0
- package/dist/templates/patterns/page/dashboard.d.ts +4 -0
- package/dist/templates/patterns/page/dashboard.js +164 -0
- package/dist/templates/patterns/page/detail.d.ts +4 -0
- package/dist/templates/patterns/page/detail.js +162 -0
- package/dist/templates/patterns/page/form.d.ts +4 -0
- package/dist/templates/patterns/page/form.js +192 -0
- package/dist/templates/patterns/page/index.d.ts +8 -0
- package/dist/templates/patterns/page/index.js +14 -0
- package/dist/templates/patterns/page/list.d.ts +4 -0
- package/dist/templates/patterns/page/list.js +138 -0
- package/dist/templates/patterns/page/settings.d.ts +4 -0
- package/dist/templates/patterns/page/settings.js +318 -0
- package/dist/templates/patterns/page/wizard.d.ts +4 -0
- package/dist/templates/patterns/page/wizard.js +293 -0
- package/dist/templates/patterns/subscriber/audit-relay.d.ts +5 -0
- package/dist/templates/patterns/subscriber/audit-relay.js +51 -0
- package/dist/templates/patterns/subscriber/cascade-deletion.d.ts +5 -0
- package/dist/templates/patterns/subscriber/cascade-deletion.js +60 -0
- package/dist/templates/patterns/subscriber/single-step-projection.d.ts +5 -0
- package/dist/templates/patterns/subscriber/single-step-projection.js +36 -0
- package/dist/templates/readme.d.ts +4 -0
- package/dist/templates/readme.js +7 -0
- package/dist/templates/subscriber-skeleton.d.ts +11 -0
- package/dist/templates/subscriber-skeleton.js +23 -0
- package/dist/utils/checksum.d.ts +18 -0
- package/dist/utils/checksum.js +39 -0
- package/dist/utils/file-helpers.d.ts +35 -0
- package/dist/utils/file-helpers.js +59 -0
- package/dist/utils/header-inject.d.ts +15 -0
- package/dist/utils/header-inject.js +93 -0
- package/dist/utils/install-file.d.ts +17 -0
- package/dist/utils/install-file.js +78 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +24 -0
- package/dist/utils/manifest.d.ts +28 -0
- package/dist/utils/manifest.js +93 -0
- package/dist/utils/s3-fetch.d.ts +25 -0
- package/dist/utils/s3-fetch.js +52 -0
- package/dist/utils/scaffold-config.d.ts +95 -0
- package/dist/utils/scaffold-config.js +53 -0
- package/dist/utils/token-substitution.d.ts +35 -0
- package/dist/utils/token-substitution.js +52 -0
- package/dist/utils/zod-extractor.d.ts +10 -0
- package/dist/utils/zod-extractor.js +28 -0
- package/package.json +43 -0
- package/src/__tests__/build-registry.test.ts +115 -0
- package/src/__tests__/build-types.test.ts +100 -0
- package/src/__tests__/builder/walkDomainDir.test.ts +140 -0
- package/src/__tests__/commands/add-domain.test.ts +174 -0
- package/src/__tests__/commands/add-flow.test.ts +69 -0
- package/src/__tests__/commands/add-module.test.ts +207 -0
- package/src/__tests__/commands/build-flows.test.ts +154 -0
- package/src/__tests__/commands/check-hashes.test.ts +142 -0
- package/src/__tests__/commands/create-project.test.ts +712 -0
- package/src/__tests__/commands/upgrade.test.ts +762 -0
- package/src/__tests__/doctor.test.ts +291 -0
- package/src/__tests__/hydrate-local-ctx.test.ts +71 -0
- package/src/__tests__/load-domain-module-config.test.ts +10 -0
- package/src/__tests__/mount-routes.test.ts +100 -0
- package/src/__tests__/scaffold-src/part-a-layout.test.ts +46 -0
- package/src/__tests__/scripts/package-scaffold.test.ts +45 -0
- package/src/__tests__/utils/checksum.test.ts +90 -0
- package/src/__tests__/utils/header-inject.test.ts +89 -0
- package/src/__tests__/utils/install-file.test.ts +207 -0
- package/src/__tests__/utils/manifest.test.ts +229 -0
- package/src/__tests__/utils/token-substitution.test.ts +134 -0
- package/src/__tests__/validate.test.ts +18 -0
- package/src/builder/build-registry.ts +237 -0
- package/src/builder/build-types.ts +36 -0
- package/src/builder/load-domain-module-config.ts +43 -0
- package/src/builder/load-module.ts +133 -0
- package/src/builder/zod-to-json.ts +16 -0
- package/src/cli.ts +387 -0
- package/src/commands/add-api.ts +145 -0
- package/src/commands/add-component.ts +116 -0
- package/src/commands/add-domain.ts +100 -0
- package/src/commands/add-flow.ts +136 -0
- package/src/commands/add-hook.ts +100 -0
- package/src/commands/add-migration.ts +129 -0
- package/src/commands/add-module.ts +347 -0
- package/src/commands/add-page.ts +242 -0
- package/src/commands/add-seed-page.ts +338 -0
- package/src/commands/add-subscriber.ts +123 -0
- package/src/commands/add-test.ts +328 -0
- package/src/commands/audit.ts +217 -0
- package/src/commands/build-catalog.ts +181 -0
- package/src/commands/build-flows.ts +198 -0
- package/src/commands/build.ts +59 -0
- package/src/commands/check-hashes.ts +150 -0
- package/src/commands/create-project.ts +367 -0
- package/src/commands/dev.ts +47 -0
- package/src/commands/doctor.ts +1362 -0
- package/src/commands/explain.ts +209 -0
- package/src/commands/power-tune.ts +112 -0
- package/src/commands/reseed-page.ts +410 -0
- package/src/commands/show-dns.ts +139 -0
- package/src/commands/show.ts +179 -0
- package/src/commands/test.ts +91 -0
- package/src/commands/upgrade-backend.ts +72 -0
- package/src/commands/upgrade-event.ts +175 -0
- package/src/commands/upgrade-frontend.ts +135 -0
- package/src/commands/upgrade.ts +668 -0
- package/src/commands/validate.ts +208 -0
- package/src/runtime-stubs/hydrate-local-ctx.ts +102 -0
- package/src/runtime-stubs/load-secrets.ts +40 -0
- package/src/server/api-server.ts +34 -0
- package/src/server/mount-routes.ts +68 -0
- package/src/templates/api-skeleton.ts +72 -0
- package/src/templates/claude-md.ts +7 -0
- package/src/templates/dashboard-pages/account/api-keys.tsx +107 -0
- package/src/templates/dashboard-pages/account/audit-log.tsx +60 -0
- package/src/templates/dashboard-pages/account/index.ts +5 -0
- package/src/templates/dashboard-pages/account/members.tsx +107 -0
- package/src/templates/dashboard-pages/account/profile.tsx +53 -0
- package/src/templates/dashboard-pages/account/workspace-settings.tsx +64 -0
- package/src/templates/dashboard-pages/auth/accept-invitation.tsx +62 -0
- package/src/templates/dashboard-pages/auth/choose-org.tsx +66 -0
- package/src/templates/dashboard-pages/auth/forgot-password.tsx +69 -0
- package/src/templates/dashboard-pages/auth/index.ts +10 -0
- package/src/templates/dashboard-pages/auth/login.tsx +75 -0
- package/src/templates/dashboard-pages/auth/mfa-setup.tsx +56 -0
- package/src/templates/dashboard-pages/auth/mfa-verify.tsx +49 -0
- package/src/templates/dashboard-pages/auth/reset-password.tsx +64 -0
- package/src/templates/dashboard-pages/auth/sign-out.tsx +20 -0
- package/src/templates/dashboard-pages/auth/signup.tsx +71 -0
- package/src/templates/dashboard-pages/auth/verify-email.tsx +53 -0
- package/src/templates/dashboard-pages/index.ts +22 -0
- package/src/templates/dashboard-pages/shell/dashboard-home.tsx +41 -0
- package/src/templates/dashboard-pages/shell/forbidden.tsx +46 -0
- package/src/templates/dashboard-pages/shell/index.ts +5 -0
- package/src/templates/dashboard-pages/shell/maintenance.tsx +27 -0
- package/src/templates/dashboard-pages/shell/not-found.tsx +34 -0
- package/src/templates/dashboard-pages/shell/server-error.tsx +44 -0
- package/src/templates/domain-config.ts +20 -0
- package/src/templates/events-skeleton.ts +24 -0
- package/src/templates/patterns/api/create-with-event.ts +72 -0
- package/src/templates/patterns/api/idempotent-mutation.ts +102 -0
- package/src/templates/patterns/api/paginated-list.ts +74 -0
- package/src/templates/patterns/api/simple-crud.ts +100 -0
- package/src/templates/patterns/api/system-admin.ts +101 -0
- package/src/templates/patterns/api/webhook-receiver-style.ts +128 -0
- package/src/templates/patterns/component/index.ts +833 -0
- package/src/templates/patterns/flow/autonomous-remediation.ts +101 -0
- package/src/templates/patterns/flow/fan-out-reaction.ts +56 -0
- package/src/templates/patterns/flow/human-in-loop.ts +93 -0
- package/src/templates/patterns/flow/multi-step-orchestration.ts +70 -0
- package/src/templates/patterns/hook/index.ts +382 -0
- package/src/templates/patterns/migration/column-add.ts +54 -0
- package/src/templates/patterns/migration/event-store.ts +99 -0
- package/src/templates/patterns/migration/index.ts +54 -0
- package/src/templates/patterns/migration/rls-policy.ts +80 -0
- package/src/templates/patterns/migration/tenant-scoped-table.ts +83 -0
- package/src/templates/patterns/page/dashboard.ts +164 -0
- package/src/templates/patterns/page/detail.ts +162 -0
- package/src/templates/patterns/page/form.ts +192 -0
- package/src/templates/patterns/page/index.ts +17 -0
- package/src/templates/patterns/page/list.ts +138 -0
- package/src/templates/patterns/page/settings.ts +318 -0
- package/src/templates/patterns/page/wizard.ts +293 -0
- package/src/templates/patterns/subscriber/audit-relay.ts +54 -0
- package/src/templates/patterns/subscriber/cascade-deletion.ts +63 -0
- package/src/templates/patterns/subscriber/single-step-projection.ts +39 -0
- package/src/templates/readme.ts +7 -0
- package/src/templates/subscriber-skeleton.ts +28 -0
- package/src/utils/checksum.ts +43 -0
- package/src/utils/file-helpers.ts +91 -0
- package/src/utils/header-inject.ts +126 -0
- package/src/utils/install-file.ts +99 -0
- package/src/utils/logger.ts +26 -0
- package/src/utils/manifest.ts +137 -0
- package/src/utils/s3-fetch.ts +91 -0
- package/src/utils/scaffold-config.ts +172 -0
- package/src/utils/token-substitution.ts +80 -0
- package/src/utils/zod-extractor.ts +31 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paginated list hook pattern — uses `useInfiniteQuery` from TanStack Query for paginated lists.
|
|
3
|
+
*/
|
|
4
|
+
export function paginatedListHookTemplate(name) {
|
|
5
|
+
const hookName = `use${capitalize(name)}`;
|
|
6
|
+
return `import { useInfiniteQuery } from '@tanstack/react-query';
|
|
7
|
+
import { useTib } from '@/hooks/useTib';
|
|
8
|
+
|
|
9
|
+
/** Request shape for paginated list endpoints. */
|
|
10
|
+
export interface ${capitalize(name)}ListRequest {
|
|
11
|
+
workspaceId: string;
|
|
12
|
+
status?: string;
|
|
13
|
+
search?: string;
|
|
14
|
+
cursor?: string;
|
|
15
|
+
limit?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Single item in the paginated response. */
|
|
19
|
+
export interface ${capitalize(name)}Item {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
status: string;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
updatedAt: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Paginated response from the API. */
|
|
28
|
+
export interface ${capitalize(name)}ListResponse {
|
|
29
|
+
items: ${capitalize(name)}Item[];
|
|
30
|
+
nextCursor: string | null;
|
|
31
|
+
totalCount: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Infinite-query hook for paginated ${name} lists.
|
|
36
|
+
*
|
|
37
|
+
* Uses TanStack Query \`useInfiniteQuery\` with cursor-based pagination.
|
|
38
|
+
* Each page is fetched as the user scrolls, keeping memory bounded.
|
|
39
|
+
*/
|
|
40
|
+
export function ${hookName}(args: ${capitalize(name)}ListRequest) {
|
|
41
|
+
const tib = useTib();
|
|
42
|
+
|
|
43
|
+
return useInfiniteQuery<${capitalize(name)}ListResponse, Error>({
|
|
44
|
+
queryKey: ['${name}-list', args.workspaceId, args.status, args.search],
|
|
45
|
+
queryFn: ({ pageParam }) =>
|
|
46
|
+
tib.data.list({
|
|
47
|
+
workspaceId: args.workspaceId,
|
|
48
|
+
status: args.status,
|
|
49
|
+
search: args.search,
|
|
50
|
+
cursor: pageParam as string | undefined,
|
|
51
|
+
limit: args.limit ?? 20,
|
|
52
|
+
}),
|
|
53
|
+
initialPageParam: undefined as string | undefined,
|
|
54
|
+
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
|
|
55
|
+
staleTime: 10_000,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
`;
|
|
59
|
+
}
|
|
60
|
+
export function mutationHookTemplate(name) {
|
|
61
|
+
const hookName = `use${capitalize(name)}`;
|
|
62
|
+
return `import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
63
|
+
import { useTib } from '@/hooks/useTib';
|
|
64
|
+
import { toast } from 'sonner';
|
|
65
|
+
|
|
66
|
+
/** Input shape for the mutation. */
|
|
67
|
+
export interface ${capitalize(name)}Input {
|
|
68
|
+
workspaceId: string;
|
|
69
|
+
/** Fields to create or update. */
|
|
70
|
+
values: Record<string, unknown>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Response from the API after a successful mutation. */
|
|
74
|
+
export interface ${capitalize(name)}Response {
|
|
75
|
+
id: string;
|
|
76
|
+
ok: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Mutation hook for creating or updating ${name} entities.
|
|
81
|
+
*
|
|
82
|
+
* Uses TanStack Query \`useMutation\` with optimistic update support,
|
|
83
|
+
* automatic error rollback, and toast notifications.
|
|
84
|
+
*/
|
|
85
|
+
export function ${hookName}() {
|
|
86
|
+
const tib = useTib();
|
|
87
|
+
const queryClient = useQueryClient();
|
|
88
|
+
|
|
89
|
+
return useMutation<${capitalize(name)}Response, Error, ${capitalize(name)}Input>({
|
|
90
|
+
mutationFn: (input: ${capitalize(name)}Input) =>
|
|
91
|
+
tib.data.upsert({
|
|
92
|
+
workspaceId: input.workspaceId,
|
|
93
|
+
...input.values,
|
|
94
|
+
}),
|
|
95
|
+
|
|
96
|
+
onMutate: async (input) => {
|
|
97
|
+
// Cancel outgoing refetches so they don't overwrite the optimistic update
|
|
98
|
+
const key = ['${name}-list', input.workspaceId];
|
|
99
|
+
await queryClient.cancelQueries({ queryKey: key });
|
|
100
|
+
|
|
101
|
+
// Snapshot the previous value for rollback
|
|
102
|
+
const previous = queryClient.getQueryData(key);
|
|
103
|
+
return { previous, key };
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
onError: (err, input, context) => {
|
|
107
|
+
// Roll back to the previous value on error
|
|
108
|
+
if (context?.previous) {
|
|
109
|
+
queryClient.setQueryData(context.key, context.previous);
|
|
110
|
+
}
|
|
111
|
+
toast.error('Operation failed', {
|
|
112
|
+
description: err.message,
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
onSuccess: (_data, input) => {
|
|
117
|
+
toast.success('Operation successful');
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
onSettled: (_data, _error, input) => {
|
|
121
|
+
// Always refetch to ensure cache is fresh
|
|
122
|
+
queryClient.invalidateQueries({
|
|
123
|
+
queryKey: ['${name}', input.workspaceId],
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Mutation hook for deleting ${name} entities.
|
|
131
|
+
*/
|
|
132
|
+
export function useDelete${capitalize(name)}() {
|
|
133
|
+
const tib = useTib();
|
|
134
|
+
const queryClient = useQueryClient();
|
|
135
|
+
|
|
136
|
+
return useMutation<void, Error, { workspaceId: string; id: string }>({
|
|
137
|
+
mutationFn: ({ workspaceId, id }) =>
|
|
138
|
+
tib.data.delete({ workspaceId, id }),
|
|
139
|
+
|
|
140
|
+
onSuccess: (_data, { workspaceId }) => {
|
|
141
|
+
toast.success('Deleted');
|
|
142
|
+
queryClient.invalidateQueries({
|
|
143
|
+
queryKey: ['${name}', workspaceId],
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
onError: (err) => {
|
|
148
|
+
toast.error('Delete failed', {
|
|
149
|
+
description: err.message,
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
`;
|
|
155
|
+
}
|
|
156
|
+
export function featureFlagHookTemplate(name) {
|
|
157
|
+
const hookName = `use${capitalize(name)}`;
|
|
158
|
+
return `import { useQuery } from '@tanstack/react-query';
|
|
159
|
+
import { useTib } from '@/hooks/useTib';
|
|
160
|
+
|
|
161
|
+
/** W6-style error shape returned by the TIB SDK. */
|
|
162
|
+
interface TibErrorShape {
|
|
163
|
+
code?: string;
|
|
164
|
+
traceId?: string;
|
|
165
|
+
message: string;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
interface FeatureFlagResult {
|
|
169
|
+
enabled: boolean;
|
|
170
|
+
config?: Record<string, unknown>;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Hook that checks a feature flag via the TIB SDK.
|
|
175
|
+
*
|
|
176
|
+
* Uses TanStack Query with a 60-second stale time (matching the
|
|
177
|
+
* backend flag cache TTL). Returns \`{ enabled: false }\` on error
|
|
178
|
+
* so the UI degrades safely when the flags service is unreachable.
|
|
179
|
+
*/
|
|
180
|
+
export function ${hookName}(flagKey: string): {
|
|
181
|
+
enabled: boolean;
|
|
182
|
+
config?: Record<string, unknown>;
|
|
183
|
+
isLoading: boolean;
|
|
184
|
+
isError: boolean;
|
|
185
|
+
error: Error | null;
|
|
186
|
+
} {
|
|
187
|
+
const tib = useTib();
|
|
188
|
+
|
|
189
|
+
const { data, isLoading, isError, error } = useQuery<FeatureFlagResult, Error>({
|
|
190
|
+
queryKey: ['feature-flag', flagKey],
|
|
191
|
+
queryFn: async () => {
|
|
192
|
+
try {
|
|
193
|
+
const result = await tib.flags.get(flagKey) as { value: boolean; config?: Record<string, unknown> };
|
|
194
|
+
return { enabled: result.value, config: result.config };
|
|
195
|
+
} catch (err) {
|
|
196
|
+
// Degrade gracefully: return disabled on error
|
|
197
|
+
return { enabled: false };
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
staleTime: 60_000, // matches backend flag cache TTL
|
|
201
|
+
retry: 1,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
enabled: data?.enabled ?? false,
|
|
206
|
+
config: data?.config,
|
|
207
|
+
isLoading,
|
|
208
|
+
isError,
|
|
209
|
+
error,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
`;
|
|
213
|
+
}
|
|
214
|
+
export function subscriptionHookTemplate(name) {
|
|
215
|
+
const hookName = `use${capitalize(name)}`;
|
|
216
|
+
return `import { useEffect, useRef, useCallback } from 'react';
|
|
217
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
218
|
+
import { useTib } from '@/hooks/useTib';
|
|
219
|
+
|
|
220
|
+
/** Message envelope received over the subscription channel. */
|
|
221
|
+
interface ${capitalize(name)}Message {
|
|
222
|
+
eventId: string;
|
|
223
|
+
type: string;
|
|
224
|
+
payload: Record<string, unknown>;
|
|
225
|
+
timestamp: string;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Callback invoked for each received message. */
|
|
229
|
+
type ${capitalize(name)}Handler = (message: ${capitalize(name)}Message) => void;
|
|
230
|
+
|
|
231
|
+
/** Connection states for the subscription. */
|
|
232
|
+
type ConnectionStatus = 'connecting' | 'connected' | 'reconnecting' | 'disconnected';
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Hook for subscribing to a real-time ${name} event stream.
|
|
236
|
+
*
|
|
237
|
+
* Uses EventSource (SSE) with automatic reconnection (exponential
|
|
238
|
+
* backoff, capped at 16 s). Incoming events are dispatched to the
|
|
239
|
+
* provided handler and optionally update TanStack Query caches.
|
|
240
|
+
*
|
|
241
|
+
* Heartbeat: server emits \`ping\` every 25 s. Missing two consecutive
|
|
242
|
+
* pings triggers reconnection.
|
|
243
|
+
*/
|
|
244
|
+
export function ${hookName}(
|
|
245
|
+
workspaceId: string,
|
|
246
|
+
onMessage?: ${capitalize(name)}Handler,
|
|
247
|
+
): {
|
|
248
|
+
status: ConnectionStatus;
|
|
249
|
+
disconnect: () => void;
|
|
250
|
+
reconnect: () => void;
|
|
251
|
+
} {
|
|
252
|
+
const tib = useTib();
|
|
253
|
+
const queryClient = useQueryClient();
|
|
254
|
+
const eventSourceRef = useRef<EventSource | null>(null);
|
|
255
|
+
const reconnectAttemptRef = useRef<number>(0);
|
|
256
|
+
const reconnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
257
|
+
const lastPingRef = useRef<number>(Date.now());
|
|
258
|
+
const pingCheckTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
259
|
+
const statusRef = useRef<ConnectionStatus>('disconnected');
|
|
260
|
+
|
|
261
|
+
const connect = useCallback(() => {
|
|
262
|
+
// Clean up any existing connection
|
|
263
|
+
if (eventSourceRef.current) {
|
|
264
|
+
eventSourceRef.current.close();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
statusRef.current = reconnectAttemptRef.current > 0 ? 'reconnecting' : 'connecting';
|
|
268
|
+
|
|
269
|
+
const url = new URL('\\\${import.meta.env.VITE_API_URL}/v1/stream');
|
|
270
|
+
url.searchParams.set('workspaceId', workspaceId);
|
|
271
|
+
|
|
272
|
+
const es = new EventSource(url.toString());
|
|
273
|
+
eventSourceRef.current = es;
|
|
274
|
+
lastPingRef.current = Date.now();
|
|
275
|
+
|
|
276
|
+
es.addEventListener('ping', () => {
|
|
277
|
+
lastPingRef.current = Date.now();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
es.addEventListener('message', (event) => {
|
|
281
|
+
try {
|
|
282
|
+
const data = JSON.parse(event.data) as ${capitalize(name)}Message;
|
|
283
|
+
statusRef.current = 'connected';
|
|
284
|
+
reconnectAttemptRef.current = 0;
|
|
285
|
+
|
|
286
|
+
if (onMessage) {
|
|
287
|
+
onMessage(data);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Optionally update a query cache key with the latest event
|
|
291
|
+
queryClient.setQueryData(['${name}-stream', workspaceId], data);
|
|
292
|
+
} catch {
|
|
293
|
+
// Ignore malformed messages
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
es.onerror = () => {
|
|
298
|
+
es.close();
|
|
299
|
+
scheduleReconnect();
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
es.onopen = () => {
|
|
303
|
+
statusRef.current = 'connected';
|
|
304
|
+
reconnectAttemptRef.current = 0;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// Start ping heartbeat checker
|
|
308
|
+
if (pingCheckTimerRef.current) {
|
|
309
|
+
clearInterval(pingCheckTimerRef.current);
|
|
310
|
+
}
|
|
311
|
+
pingCheckTimerRef.current = setInterval(() => {
|
|
312
|
+
const elapsed = Date.now() - lastPingRef.current;
|
|
313
|
+
if (elapsed > 50_000) {
|
|
314
|
+
// Missed two consecutive pings (> 50 s)
|
|
315
|
+
es.close();
|
|
316
|
+
scheduleReconnect();
|
|
317
|
+
}
|
|
318
|
+
}, 25_000);
|
|
319
|
+
}, [workspaceId, onMessage, queryClient]);
|
|
320
|
+
|
|
321
|
+
const scheduleReconnect = useCallback(() => {
|
|
322
|
+
if (reconnectTimerRef.current) {
|
|
323
|
+
clearTimeout(reconnectTimerRef.current);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const attempt = reconnectAttemptRef.current;
|
|
327
|
+
const delay = Math.min(1000 * Math.pow(2, attempt), 16_000) +
|
|
328
|
+
Math.random() * 1000; // ± jitter
|
|
329
|
+
|
|
330
|
+
reconnectAttemptRef.current = attempt + 1;
|
|
331
|
+
statusRef.current = 'reconnecting';
|
|
332
|
+
|
|
333
|
+
reconnectTimerRef.current = setTimeout(() => {
|
|
334
|
+
connect();
|
|
335
|
+
}, delay);
|
|
336
|
+
}, [connect]);
|
|
337
|
+
|
|
338
|
+
const disconnect = useCallback(() => {
|
|
339
|
+
if (eventSourceRef.current) {
|
|
340
|
+
eventSourceRef.current.close();
|
|
341
|
+
eventSourceRef.current = null;
|
|
342
|
+
}
|
|
343
|
+
if (reconnectTimerRef.current) {
|
|
344
|
+
clearTimeout(reconnectTimerRef.current);
|
|
345
|
+
reconnectTimerRef.current = null;
|
|
346
|
+
}
|
|
347
|
+
if (pingCheckTimerRef.current) {
|
|
348
|
+
clearInterval(pingCheckTimerRef.current);
|
|
349
|
+
pingCheckTimerRef.current = null;
|
|
350
|
+
}
|
|
351
|
+
statusRef.current = 'disconnected';
|
|
352
|
+
}, []);
|
|
353
|
+
|
|
354
|
+
const reconnect = useCallback(() => {
|
|
355
|
+
reconnectAttemptRef.current = 0;
|
|
356
|
+
disconnect();
|
|
357
|
+
connect();
|
|
358
|
+
}, [connect, disconnect]);
|
|
359
|
+
|
|
360
|
+
useEffect(() => {
|
|
361
|
+
connect();
|
|
362
|
+
return () => disconnect();
|
|
363
|
+
}, [connect, disconnect]);
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
get status() {
|
|
367
|
+
return statusRef.current;
|
|
368
|
+
},
|
|
369
|
+
disconnect,
|
|
370
|
+
reconnect,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
`;
|
|
374
|
+
}
|
|
375
|
+
/** Capitalizes the first character of a string. */
|
|
376
|
+
function capitalize(s) {
|
|
377
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
378
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Column-add migration pattern template.
|
|
3
|
+
* Produces: ALTER TABLE to add a new column with optional default and NOT NULL constraint.
|
|
4
|
+
*/
|
|
5
|
+
export function columnAddTemplate(domain, name) {
|
|
6
|
+
const timestamp = new Date().toISOString().replace(/[-:T.]/g, '').slice(0, 14);
|
|
7
|
+
return `-- Migration: ${timestamp}_${name}
|
|
8
|
+
-- Domain: ${domain}
|
|
9
|
+
-- Pattern: column-add
|
|
10
|
+
-- Adds one or more columns to an existing table with safe defaults.
|
|
11
|
+
|
|
12
|
+
-- ── Example: add a new column ────────────────────────────────
|
|
13
|
+
-- Replace table_name, column_name, type, and default_value as needed.
|
|
14
|
+
|
|
15
|
+
-- ALTER TABLE table_name
|
|
16
|
+
-- ADD COLUMN IF NOT EXISTS column_name VARCHAR(255) NOT NULL DEFAULT 'default_value';
|
|
17
|
+
|
|
18
|
+
-- ── Backfill existing rows (if default is not sufficient) ────
|
|
19
|
+
-- UPDATE table_name
|
|
20
|
+
-- SET column_name = 'default_value'
|
|
21
|
+
-- WHERE column_name IS NULL;
|
|
22
|
+
|
|
23
|
+
-- ── Add index on new column (if frequently queried) ──────────
|
|
24
|
+
-- CREATE INDEX IF NOT EXISTS idx_table_name_column_name
|
|
25
|
+
-- ON table_name(tenant_id, column_name);
|
|
26
|
+
|
|
27
|
+
-- ── Template for adding multiple columns ─────────────────────
|
|
28
|
+
|
|
29
|
+
DO $$
|
|
30
|
+
BEGIN
|
|
31
|
+
-- Column 1
|
|
32
|
+
IF NOT EXISTS (
|
|
33
|
+
SELECT 1 FROM information_schema.columns
|
|
34
|
+
WHERE table_name = 'REPLACE_WITH_TABLE_NAME'
|
|
35
|
+
AND column_name = 'REPLACE_WITH_COLUMN_1'
|
|
36
|
+
) THEN
|
|
37
|
+
ALTER TABLE REPLACE_WITH_TABLE_NAME
|
|
38
|
+
ADD COLUMN REPLACE_WITH_COLUMN_1 TEXT;
|
|
39
|
+
END IF;
|
|
40
|
+
|
|
41
|
+
-- Column 2
|
|
42
|
+
IF NOT EXISTS (
|
|
43
|
+
SELECT 1 FROM information_schema.columns
|
|
44
|
+
WHERE table_name = 'REPLACE_WITH_TABLE_NAME'
|
|
45
|
+
AND column_name = 'REPLACE_WITH_COLUMN_2'
|
|
46
|
+
) THEN
|
|
47
|
+
ALTER TABLE REPLACE_WITH_TABLE_NAME
|
|
48
|
+
ADD COLUMN REPLACE_WITH_COLUMN_2 JSONB DEFAULT '{}'::jsonb;
|
|
49
|
+
END IF;
|
|
50
|
+
END $$;
|
|
51
|
+
`;
|
|
52
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event-store migration pattern template.
|
|
3
|
+
* Produces: CREATE TABLE for an event-store with append-only semantics,
|
|
4
|
+
* event_id sequence, aggregate root, and version tracking.
|
|
5
|
+
*/
|
|
6
|
+
export declare function eventStoreTemplate(domain: string, name: string): string;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event-store migration pattern template.
|
|
3
|
+
* Produces: CREATE TABLE for an event-store with append-only semantics,
|
|
4
|
+
* event_id sequence, aggregate root, and version tracking.
|
|
5
|
+
*/
|
|
6
|
+
export function eventStoreTemplate(domain, name) {
|
|
7
|
+
const tableName = name.replace(/-/g, '_');
|
|
8
|
+
const timestamp = new Date().toISOString().replace(/[-:T.]/g, '').slice(0, 14);
|
|
9
|
+
return `-- Migration: ${timestamp}_${name}
|
|
10
|
+
-- Domain: ${domain}
|
|
11
|
+
-- Pattern: event-store
|
|
12
|
+
-- Creates an append-only event-store table with aggregate-root tracking.
|
|
13
|
+
|
|
14
|
+
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
15
|
+
-- Event identity
|
|
16
|
+
id BIGSERIAL PRIMARY KEY,
|
|
17
|
+
event_id UUID NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
|
18
|
+
|
|
19
|
+
-- Aggregate root
|
|
20
|
+
aggregate_type VARCHAR(100) NOT NULL,
|
|
21
|
+
aggregate_id UUID NOT NULL,
|
|
22
|
+
aggregate_version INT NOT NULL DEFAULT 1,
|
|
23
|
+
|
|
24
|
+
-- Event metadata
|
|
25
|
+
event_type VARCHAR(200) NOT NULL,
|
|
26
|
+
event_version INT NOT NULL DEFAULT 1,
|
|
27
|
+
payload JSONB NOT NULL,
|
|
28
|
+
metadata JSONB DEFAULT '{}'::jsonb,
|
|
29
|
+
|
|
30
|
+
-- Tenancy
|
|
31
|
+
tenant_id UUID NOT NULL,
|
|
32
|
+
workspace_id UUID,
|
|
33
|
+
|
|
34
|
+
-- Audit
|
|
35
|
+
occurred_at TIMESTAMPTZ NOT NULL,
|
|
36
|
+
recorded_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
37
|
+
correlation_id UUID,
|
|
38
|
+
causation_id UUID,
|
|
39
|
+
|
|
40
|
+
-- Concurrency guard
|
|
41
|
+
UNIQUE(aggregate_type, aggregate_id, aggregate_version)
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
-- ── Indexes ──────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_aggregate
|
|
47
|
+
ON ${tableName}(aggregate_type, aggregate_id, aggregate_version);
|
|
48
|
+
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_tenant_occurred
|
|
50
|
+
ON ${tableName}(tenant_id, occurred_at DESC);
|
|
51
|
+
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_event_type
|
|
53
|
+
ON ${tableName}(event_type, occurred_at DESC);
|
|
54
|
+
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_correlation
|
|
56
|
+
ON ${tableName}(correlation_id)
|
|
57
|
+
WHERE correlation_id IS NOT NULL;
|
|
58
|
+
|
|
59
|
+
-- ── Row-Level Security ───────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
ALTER TABLE ${tableName} ENABLE ROW LEVEL SECURITY;
|
|
62
|
+
|
|
63
|
+
CREATE POLICY ${tableName}_tenant_isolation
|
|
64
|
+
ON ${tableName}
|
|
65
|
+
FOR SELECT
|
|
66
|
+
TO authenticated
|
|
67
|
+
USING (tenant_id = current_setting('app.current_tenant_id')::UUID);
|
|
68
|
+
|
|
69
|
+
-- ── Append-only guard ────────────────────────────────────────
|
|
70
|
+
-- No UPDATE or DELETE allowed; only INSERT and SELECT.
|
|
71
|
+
|
|
72
|
+
CREATE POLICY ${tableName}_insert_only
|
|
73
|
+
ON ${tableName}
|
|
74
|
+
FOR INSERT
|
|
75
|
+
TO authenticated
|
|
76
|
+
WITH CHECK (true);
|
|
77
|
+
|
|
78
|
+
-- ── Helper: next aggregate version ───────────────────────────
|
|
79
|
+
|
|
80
|
+
CREATE OR REPLACE FUNCTION ${tableName}_next_version(
|
|
81
|
+
p_aggregate_type VARCHAR,
|
|
82
|
+
p_aggregate_id UUID
|
|
83
|
+
)
|
|
84
|
+
RETURNS INT AS $$
|
|
85
|
+
DECLARE
|
|
86
|
+
v_next INT;
|
|
87
|
+
BEGIN
|
|
88
|
+
SELECT COALESCE(MAX(aggregate_version), 0) + 1
|
|
89
|
+
INTO v_next
|
|
90
|
+
FROM ${tableName}
|
|
91
|
+
WHERE aggregate_type = p_aggregate_type
|
|
92
|
+
AND aggregate_id = p_aggregate_id;
|
|
93
|
+
RETURN v_next;
|
|
94
|
+
END;
|
|
95
|
+
$$ LANGUAGE plpgsql;
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Index migration pattern template.
|
|
3
|
+
* Produces: CREATE INDEX statements with CONCURRENTLY option and performance notes.
|
|
4
|
+
*/
|
|
5
|
+
export function indexTemplate(domain, name) {
|
|
6
|
+
const timestamp = new Date().toISOString().replace(/[-:T.]/g, '').slice(0, 14);
|
|
7
|
+
return `-- Migration: ${timestamp}_${name}
|
|
8
|
+
-- Domain: ${domain}
|
|
9
|
+
-- Pattern: index
|
|
10
|
+
-- Creates indexes to optimise query performance.
|
|
11
|
+
--
|
|
12
|
+
-- IMPORTANT: CREATE INDEX CONCURRENTLY must run outside a transaction.
|
|
13
|
+
-- Run this migration with --no-transaction if the tooling supports it.
|
|
14
|
+
|
|
15
|
+
-- ── Single-column index ──────────────────────────────────────
|
|
16
|
+
-- CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_table_name_column
|
|
17
|
+
-- ON table_name(column_name);
|
|
18
|
+
|
|
19
|
+
-- ── Composite index (tenant-scoped queries) ──────────────────
|
|
20
|
+
-- CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_table_name_tenant_column
|
|
21
|
+
-- ON table_name(tenant_id, column_name);
|
|
22
|
+
|
|
23
|
+
-- ── Partial index (filtered) ─────────────────────────────────
|
|
24
|
+
-- CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_table_name_active
|
|
25
|
+
-- ON table_name(tenant_id, created_at DESC)
|
|
26
|
+
-- WHERE status = 'active';
|
|
27
|
+
|
|
28
|
+
-- ── GIN index for JSONB columns ──────────────────────────────
|
|
29
|
+
-- CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_table_name_metadata_gin
|
|
30
|
+
-- ON table_name USING GIN(metadata jsonb_path_ops);
|
|
31
|
+
|
|
32
|
+
-- ── Unique index (enforce business constraint) ───────────────
|
|
33
|
+
-- CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS uq_table_name_tenant_name
|
|
34
|
+
-- ON table_name(tenant_id, name)
|
|
35
|
+
-- WHERE deleted_at IS NULL;
|
|
36
|
+
|
|
37
|
+
-- ── Template: safe index creation with existence check ───────
|
|
38
|
+
|
|
39
|
+
DO $$
|
|
40
|
+
BEGIN
|
|
41
|
+
IF NOT EXISTS (
|
|
42
|
+
SELECT 1 FROM pg_indexes
|
|
43
|
+
WHERE schemaname = 'public'
|
|
44
|
+
AND tablename = 'REPLACE_WITH_TABLE_NAME'
|
|
45
|
+
AND indexname = 'REPLACE_WITH_INDEX_NAME'
|
|
46
|
+
) THEN
|
|
47
|
+
CREATE INDEX CONCURRENTLY REPLACE_WITH_INDEX_NAME
|
|
48
|
+
ON REPLACE_WITH_TABLE_NAME(REPLACE_WITH_COLUMN_NAME);
|
|
49
|
+
END IF;
|
|
50
|
+
END $$;
|
|
51
|
+
`;
|
|
52
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RLS-policy migration pattern template.
|
|
3
|
+
* Produces: ALTER TABLE to add/enable RLS policies on an existing table.
|
|
4
|
+
*/
|
|
5
|
+
export function rlsPolicyTemplate(domain, name) {
|
|
6
|
+
const tableName = name.replace(/-/g, '_');
|
|
7
|
+
const timestamp = new Date().toISOString().replace(/[-:T.]/g, '').slice(0, 14);
|
|
8
|
+
return `-- Migration: ${timestamp}_${name}
|
|
9
|
+
-- Domain: ${domain}
|
|
10
|
+
-- Pattern: rls-policy
|
|
11
|
+
-- Adds Row-Level Security policies to an existing table.
|
|
12
|
+
|
|
13
|
+
-- ── Enable RLS if not already enabled ────────────────────────
|
|
14
|
+
|
|
15
|
+
ALTER TABLE ${tableName} ENABLE ROW LEVEL SECURITY;
|
|
16
|
+
|
|
17
|
+
-- ── Tenant isolation policy ──────────────────────────────────
|
|
18
|
+
-- Ensures rows are only visible within the same tenant context.
|
|
19
|
+
|
|
20
|
+
DO $$
|
|
21
|
+
BEGIN
|
|
22
|
+
IF NOT EXISTS (
|
|
23
|
+
SELECT 1 FROM pg_policies
|
|
24
|
+
WHERE schemaname = 'public'
|
|
25
|
+
AND tablename = '${tableName}'
|
|
26
|
+
AND policyname = '${tableName}_tenant_isolation'
|
|
27
|
+
) THEN
|
|
28
|
+
CREATE POLICY ${tableName}_tenant_isolation
|
|
29
|
+
ON ${tableName}
|
|
30
|
+
FOR ALL
|
|
31
|
+
TO authenticated
|
|
32
|
+
USING (tenant_id = current_setting('app.current_tenant_id')::UUID)
|
|
33
|
+
WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::UUID);
|
|
34
|
+
END IF;
|
|
35
|
+
END $$;
|
|
36
|
+
|
|
37
|
+
-- ── Soft-delete filter policy ────────────────────────────────
|
|
38
|
+
-- Automatically excludes soft-deleted rows from SELECT queries.
|
|
39
|
+
|
|
40
|
+
DO $$
|
|
41
|
+
BEGIN
|
|
42
|
+
IF NOT EXISTS (
|
|
43
|
+
SELECT 1 FROM pg_policies
|
|
44
|
+
WHERE schemaname = 'public'
|
|
45
|
+
AND tablename = '${tableName}'
|
|
46
|
+
AND policyname = '${tableName}_soft_delete_filter'
|
|
47
|
+
) THEN
|
|
48
|
+
CREATE POLICY ${tableName}_soft_delete_filter
|
|
49
|
+
ON ${tableName}
|
|
50
|
+
FOR SELECT
|
|
51
|
+
TO authenticated
|
|
52
|
+
USING (deleted_at IS NULL);
|
|
53
|
+
END IF;
|
|
54
|
+
END $$;
|
|
55
|
+
|
|
56
|
+
-- ── Workspace-scoped access policy ───────────────────────────
|
|
57
|
+
-- Restricts access to rows belonging to the current workspace.
|
|
58
|
+
|
|
59
|
+
DO $$
|
|
60
|
+
BEGIN
|
|
61
|
+
IF NOT EXISTS (
|
|
62
|
+
SELECT 1 FROM pg_policies
|
|
63
|
+
WHERE schemaname = 'public'
|
|
64
|
+
AND tablename = '${tableName}'
|
|
65
|
+
AND policyname = '${tableName}_workspace_scope'
|
|
66
|
+
) THEN
|
|
67
|
+
CREATE POLICY ${tableName}_workspace_scope
|
|
68
|
+
ON ${tableName}
|
|
69
|
+
FOR SELECT
|
|
70
|
+
TO authenticated
|
|
71
|
+
USING (
|
|
72
|
+
workspace_id IS NULL
|
|
73
|
+
OR workspace_id = current_setting('app.current_workspace_id', true)::UUID
|
|
74
|
+
);
|
|
75
|
+
END IF;
|
|
76
|
+
END $$;
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant-scoped-table migration pattern template.
|
|
3
|
+
* Produces: CREATE TABLE with tenancy trio (tenant_id, workspace_id, org_id),
|
|
4
|
+
* RLS policy, updated_at trigger, and standard indexes.
|
|
5
|
+
*/
|
|
6
|
+
export declare function tenantScopedTableTemplate(domain: string, name: string): string;
|