@percher/core 0.4.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/dist/commands/account.d.ts +24 -14
- package/dist/commands/account.d.ts.map +1 -1
- package/dist/commands/account.js +17 -4
- package/dist/commands/account.js.map +1 -1
- package/dist/commands/admin-reconcile-routes.d.ts +18 -0
- package/dist/commands/admin-reconcile-routes.d.ts.map +1 -0
- package/dist/commands/admin-reconcile-routes.js +22 -0
- package/dist/commands/admin-reconcile-routes.js.map +1 -0
- package/dist/commands/ai-files.d.ts +5 -17
- package/dist/commands/ai-files.d.ts.map +1 -1
- package/dist/commands/ai-files.js +3 -4
- package/dist/commands/ai-files.js.map +1 -1
- package/dist/commands/alerts.d.ts +69 -0
- package/dist/commands/alerts.d.ts.map +1 -0
- package/dist/commands/alerts.js +80 -0
- package/dist/commands/alerts.js.map +1 -0
- package/dist/commands/app-resources.d.ts +30 -0
- package/dist/commands/app-resources.d.ts.map +1 -0
- package/dist/commands/app-resources.js +34 -0
- package/dist/commands/app-resources.js.map +1 -0
- package/dist/commands/app-topology.d.ts +18 -0
- package/dist/commands/app-topology.d.ts.map +1 -0
- package/dist/commands/app-topology.js +25 -0
- package/dist/commands/app-topology.js.map +1 -0
- package/dist/commands/billing.d.ts +8 -8
- package/dist/commands/billing.d.ts.map +1 -1
- package/dist/commands/billing.js +1 -1
- package/dist/commands/billing.js.map +1 -1
- package/dist/commands/continue.d.ts +1 -1
- package/dist/commands/create.d.ts +2 -12
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +1 -1
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/dashboard.d.ts +2 -8
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +1 -1
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/data-export.d.ts +2 -8
- package/dist/commands/data-export.d.ts.map +1 -1
- package/dist/commands/data-export.js +1 -1
- package/dist/commands/data-export.js.map +1 -1
- package/dist/commands/data.d.ts +2 -8
- package/dist/commands/data.d.ts.map +1 -1
- package/dist/commands/data.js +1 -1
- package/dist/commands/data.js.map +1 -1
- package/dist/commands/delete.d.ts +2 -8
- package/dist/commands/delete.d.ts.map +1 -1
- package/dist/commands/delete.js +1 -1
- package/dist/commands/delete.js.map +1 -1
- package/dist/commands/deploys.d.ts +4 -28
- package/dist/commands/deploys.d.ts.map +1 -1
- package/dist/commands/deploys.js +1 -1
- package/dist/commands/deploys.js.map +1 -1
- package/dist/commands/dev.d.ts +2 -6
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +1 -1
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/diagnose.d.ts +2 -22
- package/dist/commands/diagnose.d.ts.map +1 -1
- package/dist/commands/diagnose.js +1 -1
- package/dist/commands/diagnose.js.map +1 -1
- package/dist/commands/doctor.d.ts +10 -35
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +12 -4
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/domains.d.ts +5 -27
- package/dist/commands/domains.d.ts.map +1 -1
- package/dist/commands/domains.js +1 -1
- package/dist/commands/domains.js.map +1 -1
- package/dist/commands/env-scan.js +1 -1
- package/dist/commands/env-scan.js.map +1 -1
- package/dist/commands/env.d.ts +4 -20
- package/dist/commands/env.d.ts.map +1 -1
- package/dist/commands/env.js +1 -1
- package/dist/commands/env.js.map +1 -1
- package/dist/commands/export.d.ts +1 -1
- package/dist/commands/forgejo.d.ts +45 -0
- package/dist/commands/forgejo.d.ts.map +1 -0
- package/dist/commands/forgejo.js +125 -0
- package/dist/commands/forgejo.js.map +1 -0
- package/dist/commands/generate.d.ts +2 -6
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +1 -1
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/github.d.ts +4 -15
- package/dist/commands/github.d.ts.map +1 -1
- package/dist/commands/github.js +17 -1
- package/dist/commands/github.js.map +1 -1
- package/dist/commands/import-project.d.ts +13 -9
- package/dist/commands/import-project.d.ts.map +1 -1
- package/dist/commands/import-project.js +73 -22
- package/dist/commands/import-project.js.map +1 -1
- package/dist/commands/init.d.ts +26 -11
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +103 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/insights.d.ts +2 -6
- package/dist/commands/insights.d.ts.map +1 -1
- package/dist/commands/insights.js +1 -1
- package/dist/commands/insights.js.map +1 -1
- package/dist/commands/login.d.ts +2 -8
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +22 -1
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/logs.d.ts +25 -10
- package/dist/commands/logs.d.ts.map +1 -1
- package/dist/commands/logs.js +65 -5
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/mcp.d.ts +2 -2
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +1 -1
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/migrate-supabase-map.d.ts +171 -0
- package/dist/commands/migrate-supabase-map.d.ts.map +1 -0
- package/dist/commands/migrate-supabase-map.js +452 -0
- package/dist/commands/migrate-supabase-map.js.map +1 -0
- package/dist/commands/migrate-supabase-schema.d.ts +67 -0
- package/dist/commands/migrate-supabase-schema.d.ts.map +1 -0
- package/dist/commands/migrate-supabase-schema.js +321 -0
- package/dist/commands/migrate-supabase-schema.js.map +1 -0
- package/dist/commands/migrate-supabase-scripts.d.ts +64 -0
- package/dist/commands/migrate-supabase-scripts.d.ts.map +1 -0
- package/dist/commands/migrate-supabase-scripts.js +564 -0
- package/dist/commands/migrate-supabase-scripts.js.map +1 -0
- package/dist/commands/migrate-supabase-sdk.d.ts +133 -0
- package/dist/commands/migrate-supabase-sdk.d.ts.map +1 -0
- package/dist/commands/migrate-supabase-sdk.js +1119 -0
- package/dist/commands/migrate-supabase-sdk.js.map +1 -0
- package/dist/commands/migrate-supabase-walker.d.ts +93 -0
- package/dist/commands/migrate-supabase-walker.d.ts.map +1 -0
- package/dist/commands/migrate-supabase-walker.js +413 -0
- package/dist/commands/migrate-supabase-walker.js.map +1 -0
- package/dist/commands/migrate-supabase.d.ts +81 -0
- package/dist/commands/migrate-supabase.d.ts.map +1 -0
- package/dist/commands/migrate-supabase.js +579 -0
- package/dist/commands/migrate-supabase.js.map +1 -0
- package/dist/commands/open.d.ts +2 -6
- package/dist/commands/open.d.ts.map +1 -1
- package/dist/commands/open.js +1 -1
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/publish-api-error.d.ts +46 -0
- package/dist/commands/publish-api-error.d.ts.map +1 -0
- package/dist/commands/publish-api-error.js +307 -0
- package/dist/commands/publish-api-error.js.map +1 -0
- package/dist/commands/publish.d.ts +40 -17
- package/dist/commands/publish.d.ts.map +1 -1
- package/dist/commands/publish.js +115 -8
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/push.d.ts +2 -12
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +2 -2
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/redeploy.d.ts +2 -8
- package/dist/commands/redeploy.d.ts.map +1 -1
- package/dist/commands/redeploy.js +2 -2
- package/dist/commands/redeploy.js.map +1 -1
- package/dist/commands/rename.d.ts +2 -8
- package/dist/commands/rename.d.ts.map +1 -1
- package/dist/commands/rename.js +1 -1
- package/dist/commands/rename.js.map +1 -1
- package/dist/commands/reproduce.d.ts +2 -8
- package/dist/commands/reproduce.d.ts.map +1 -1
- package/dist/commands/reproduce.js +1 -1
- package/dist/commands/reproduce.js.map +1 -1
- package/dist/commands/reset-superuser.d.ts +2 -16
- package/dist/commands/reset-superuser.d.ts.map +1 -1
- package/dist/commands/reset-superuser.js +1 -1
- package/dist/commands/reset-superuser.js.map +1 -1
- package/dist/commands/restore.d.ts +7 -22
- package/dist/commands/restore.d.ts.map +1 -1
- package/dist/commands/restore.js +1 -1
- package/dist/commands/restore.js.map +1 -1
- package/dist/commands/resume.d.ts +2 -6
- package/dist/commands/resume.d.ts.map +1 -1
- package/dist/commands/resume.js +1 -1
- package/dist/commands/resume.js.map +1 -1
- package/dist/commands/rollback.d.ts +2 -8
- package/dist/commands/rollback.d.ts.map +1 -1
- package/dist/commands/rollback.js +1 -1
- package/dist/commands/rollback.js.map +1 -1
- package/dist/commands/sharing.d.ts +48 -0
- package/dist/commands/sharing.d.ts.map +1 -0
- package/dist/commands/sharing.js +85 -0
- package/dist/commands/sharing.js.map +1 -0
- package/dist/commands/status.d.ts +2 -6
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/transfers.d.ts +34 -0
- package/dist/commands/transfers.d.ts.map +1 -0
- package/dist/commands/transfers.js +62 -0
- package/dist/commands/transfers.js.map +1 -0
- package/dist/commands/unsuspend.d.ts +2 -6
- package/dist/commands/unsuspend.d.ts.map +1 -1
- package/dist/commands/unsuspend.js +1 -1
- package/dist/commands/unsuspend.js.map +1 -1
- package/dist/commands/versions.d.ts +2 -6
- package/dist/commands/versions.d.ts.map +1 -1
- package/dist/commands/versions.js +1 -1
- package/dist/commands/versions.js.map +1 -1
- package/dist/commands/wait-deploy.d.ts +2 -12
- package/dist/commands/wait-deploy.d.ts.map +1 -1
- package/dist/commands/wait-deploy.js +1 -1
- package/dist/commands/wait-deploy.js.map +1 -1
- package/dist/context.d.ts +15 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/detect.d.ts +11 -0
- package/dist/detect.d.ts.map +1 -1
- package/dist/detect.js +31 -8
- package/dist/detect.js.map +1 -1
- package/dist/env-scan-source.js +1 -1
- package/dist/env-scan-source.js.map +1 -1
- package/dist/error-classifier.d.ts +17 -0
- package/dist/error-classifier.d.ts.map +1 -1
- package/dist/error-classifier.js +94 -8
- package/dist/error-classifier.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +63 -49
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +56 -42
- package/dist/index.js.map +1 -1
- package/dist/plans.d.ts +61 -5
- package/dist/plans.d.ts.map +1 -1
- package/dist/plans.js +78 -18
- package/dist/plans.js.map +1 -1
- package/dist/recovery.d.ts +60 -3
- package/dist/recovery.d.ts.map +1 -1
- package/dist/recovery.js +22 -0
- package/dist/recovery.js.map +1 -1
- package/dist/static-docker.d.ts +77 -0
- package/dist/static-docker.d.ts.map +1 -0
- package/dist/static-docker.js +105 -0
- package/dist/static-docker.js.map +1 -0
- package/dist/tarball.js +1 -1
- package/dist/tarball.js.map +1 -1
- package/dist/templates/ai-files/cursor-percher-mdc.d.ts.map +1 -1
- package/dist/templates/ai-files/cursor-percher-mdc.js +12 -9
- package/dist/templates/ai-files/cursor-percher-mdc.js.map +1 -1
- package/dist/templates.js +11 -11
- package/dist/templates.js.map +1 -1
- package/dist/watcher.js +1 -1
- package/dist/watcher.js.map +1 -1
- package/package.json +6 -2
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Context } from "../context";
|
|
3
|
+
import { type AskUserRecovery, type NoneRecovery, type ToolRecovery } from "../recovery";
|
|
4
|
+
/**
|
|
5
|
+
* FUTURE9 Phase A — Supabase schema introspection.
|
|
6
|
+
*
|
|
7
|
+
* Client-side only: the Supabase personal access token is a
|
|
8
|
+
* high-privilege credential (full project access). We never route
|
|
9
|
+
* it through Percher's servers — the call goes from the user's
|
|
10
|
+
* process directly to api.supabase.com. See decision 1 in
|
|
11
|
+
* docs/plans/FUTURE9_fas-8-lovable-migration-plan.md.
|
|
12
|
+
*
|
|
13
|
+
* This command performs no file writes; it returns a structured
|
|
14
|
+
* schema description for later phases (B–D) to build on, and so
|
|
15
|
+
* the user can sanity-check what would migrate before committing.
|
|
16
|
+
*/
|
|
17
|
+
export declare const inspectSupabaseInputSchema: z.ZodObject<{
|
|
18
|
+
projectRef: z.ZodString;
|
|
19
|
+
token: z.ZodString;
|
|
20
|
+
}, z.core.$strip>;
|
|
21
|
+
export type InspectSupabaseInput = z.infer<typeof inspectSupabaseInputSchema>;
|
|
22
|
+
export interface SupabaseForeignKey {
|
|
23
|
+
table: string;
|
|
24
|
+
column: string;
|
|
25
|
+
}
|
|
26
|
+
export interface SupabaseColumn {
|
|
27
|
+
name: string;
|
|
28
|
+
postgresType: string;
|
|
29
|
+
isNullable: boolean;
|
|
30
|
+
hasDefault: boolean;
|
|
31
|
+
defaultExpr: string | null;
|
|
32
|
+
isPrimaryKey: boolean;
|
|
33
|
+
foreignKey: SupabaseForeignKey | null;
|
|
34
|
+
}
|
|
35
|
+
export interface SupabaseRlsPolicy {
|
|
36
|
+
name: string;
|
|
37
|
+
command: string;
|
|
38
|
+
usingExpr: string | null;
|
|
39
|
+
withCheckExpr: string | null;
|
|
40
|
+
}
|
|
41
|
+
export interface SupabaseTable {
|
|
42
|
+
schema: string;
|
|
43
|
+
name: string;
|
|
44
|
+
columns: SupabaseColumn[];
|
|
45
|
+
rlsPolicies: SupabaseRlsPolicy[];
|
|
46
|
+
estimatedRowCount: number;
|
|
47
|
+
}
|
|
48
|
+
export interface SupabaseStorageBucket {
|
|
49
|
+
id: string;
|
|
50
|
+
name: string;
|
|
51
|
+
public: boolean;
|
|
52
|
+
}
|
|
53
|
+
export type InspectSupabaseStatus = "ok" | "failed";
|
|
54
|
+
export interface InspectSupabaseError {
|
|
55
|
+
code: "auth_failed" | "project_not_found" | "project_paused" | "query_timeout" | "network_error" | "unexpected_response";
|
|
56
|
+
title: string;
|
|
57
|
+
explanation: string;
|
|
58
|
+
suggestion: string;
|
|
59
|
+
}
|
|
60
|
+
export interface InspectSupabaseResult {
|
|
61
|
+
status: InspectSupabaseStatus;
|
|
62
|
+
projectRef: string;
|
|
63
|
+
tables: SupabaseTable[];
|
|
64
|
+
authUsersCount: number | null;
|
|
65
|
+
storageBuckets: SupabaseStorageBucket[];
|
|
66
|
+
warnings: string[];
|
|
67
|
+
error?: InspectSupabaseError;
|
|
68
|
+
recovery: ToolRecovery | NoneRecovery | AskUserRecovery;
|
|
69
|
+
summary: string;
|
|
70
|
+
}
|
|
71
|
+
export declare function inspectSupabase(ctx: Context, input: InspectSupabaseInput): Promise<InspectSupabaseResult>;
|
|
72
|
+
/**
|
|
73
|
+
* Helper for downstream code (phases B–D) to filter tables to just the
|
|
74
|
+
* ones we'd migrate — i.e. nothing in a Supabase-managed system schema.
|
|
75
|
+
* Phase A returns these already filtered, but the predicate is exported
|
|
76
|
+
* so a caller can re-filter a result they've stored.
|
|
77
|
+
*/
|
|
78
|
+
export declare function isMigratableTable(table: {
|
|
79
|
+
schema: string;
|
|
80
|
+
}): boolean;
|
|
81
|
+
//# sourceMappingURL=migrate-supabase.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate-supabase.d.ts","sourceRoot":"","sources":["../../src/commands/migrate-supabase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,YAAY,EAGjB,KAAK,YAAY,EAClB,MAAM,aAAa,CAAC;AAErB;;;;;;;;;;;;GAYG;AAEH,eAAO,MAAM,0BAA0B;;;iBAarC,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAY9E,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,OAAO,CAAC;IACtB,UAAU,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,WAAW,EAAE,iBAAiB,EAAE,CAAC;IACjC,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,MAAM,qBAAqB,GAAG,IAAI,GAAG,QAAQ,CAAC;AAEpD,MAAM,WAAW,oBAAoB;IACnC,IAAI,EACA,aAAa,GACb,mBAAmB,GACnB,gBAAgB,GAChB,eAAe,GACf,eAAe,GACf,qBAAqB,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,qBAAqB,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,cAAc,EAAE,qBAAqB,EAAE,CAAC;IACxC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,QAAQ,EAAE,YAAY,GAAG,YAAY,GAAG,eAAe,CAAC;IACxD,OAAO,EAAE,MAAM,CAAC;CACjB;AA4JD,wBAAsB,eAAe,CACnC,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,oBAAoB,GAC1B,OAAO,CAAC,qBAAqB,CAAC,CAsLhC;AAyQD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAEpE"}
|
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { recoveryAsk, recoveryNone, } from "../recovery";
|
|
3
|
+
/**
|
|
4
|
+
* FUTURE9 Phase A — Supabase schema introspection.
|
|
5
|
+
*
|
|
6
|
+
* Client-side only: the Supabase personal access token is a
|
|
7
|
+
* high-privilege credential (full project access). We never route
|
|
8
|
+
* it through Percher's servers — the call goes from the user's
|
|
9
|
+
* process directly to api.supabase.com. See decision 1 in
|
|
10
|
+
* docs/plans/FUTURE9_fas-8-lovable-migration-plan.md.
|
|
11
|
+
*
|
|
12
|
+
* This command performs no file writes; it returns a structured
|
|
13
|
+
* schema description for later phases (B–D) to build on, and so
|
|
14
|
+
* the user can sanity-check what would migrate before committing.
|
|
15
|
+
*/
|
|
16
|
+
export const inspectSupabaseInputSchema = z.object({
|
|
17
|
+
projectRef: z
|
|
18
|
+
.string()
|
|
19
|
+
.min(1, "projectRef is required")
|
|
20
|
+
.describe("Supabase project reference (the 20-char id from the project URL — e.g. `abcdefghijklmnopqrst`)."),
|
|
21
|
+
token: z
|
|
22
|
+
.string()
|
|
23
|
+
.min(1, "token is required")
|
|
24
|
+
.describe("Supabase personal access token (sbp_...). Never leaves the user's machine — sent directly to api.supabase.com, not via Percher."),
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* Production Supabase Management API host. Hardcoded — not overridable
|
|
28
|
+
* via the public input schema or CLI flags, because the input carries
|
|
29
|
+
* a high-privilege personal access token and the tool's contract
|
|
30
|
+
* promises the call goes only to api.supabase.com. Tests intercept
|
|
31
|
+
* fetch at the global level instead (they don't need to redirect to
|
|
32
|
+
* a different host to assert behavior — the mock catches every call).
|
|
33
|
+
*/
|
|
34
|
+
const SUPABASE_API_BASE_URL = "https://api.supabase.com";
|
|
35
|
+
// User-schema reserved set — these are Supabase platform-managed
|
|
36
|
+
// or Postgres internals we don't migrate. Mirror this in the SQL
|
|
37
|
+
// WHERE clause; keep the TS list for downstream phases that need
|
|
38
|
+
// to filter post-hoc.
|
|
39
|
+
const SYSTEM_SCHEMAS = new Set([
|
|
40
|
+
"pg_catalog",
|
|
41
|
+
"information_schema",
|
|
42
|
+
"pgsodium",
|
|
43
|
+
"pgsodium_masks",
|
|
44
|
+
"vault",
|
|
45
|
+
"extensions",
|
|
46
|
+
"pgbouncer",
|
|
47
|
+
"realtime",
|
|
48
|
+
"_realtime",
|
|
49
|
+
"graphql",
|
|
50
|
+
"graphql_public",
|
|
51
|
+
"supabase_functions",
|
|
52
|
+
"supabase_migrations",
|
|
53
|
+
"auth",
|
|
54
|
+
"storage",
|
|
55
|
+
"net",
|
|
56
|
+
]);
|
|
57
|
+
const INTROSPECTION_SQL = `
|
|
58
|
+
WITH
|
|
59
|
+
pks AS (
|
|
60
|
+
SELECT tc.table_schema, tc.table_name, kcu.column_name
|
|
61
|
+
FROM information_schema.table_constraints tc
|
|
62
|
+
JOIN information_schema.key_column_usage kcu
|
|
63
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
64
|
+
AND tc.table_schema = kcu.table_schema
|
|
65
|
+
WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
66
|
+
),
|
|
67
|
+
fks AS (
|
|
68
|
+
SELECT tc.table_schema, tc.table_name, kcu.column_name,
|
|
69
|
+
ccu.table_name AS foreign_table,
|
|
70
|
+
ccu.column_name AS foreign_column
|
|
71
|
+
FROM information_schema.table_constraints tc
|
|
72
|
+
JOIN information_schema.key_column_usage kcu
|
|
73
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
74
|
+
AND tc.table_schema = kcu.table_schema
|
|
75
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
76
|
+
ON tc.constraint_name = ccu.constraint_name
|
|
77
|
+
AND tc.table_schema = ccu.table_schema
|
|
78
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
79
|
+
),
|
|
80
|
+
cols AS (
|
|
81
|
+
SELECT
|
|
82
|
+
c.table_schema, c.table_name, c.column_name,
|
|
83
|
+
-- information_schema.columns reports array columns as data_type='ARRAY'
|
|
84
|
+
-- with the element type hidden in udt_name (prefixed with an
|
|
85
|
+
-- underscore — e.g. text[] → udt_name='_text'). Format here so the
|
|
86
|
+
-- result carries the user-friendly Postgres syntax (text[], int4[])
|
|
87
|
+
-- and so downstream type-mapping (Phase B) can pattern-match on the
|
|
88
|
+
-- ANSI shape rather than the catalog quirk.
|
|
89
|
+
CASE
|
|
90
|
+
WHEN c.data_type = 'ARRAY' THEN substr(c.udt_name, 2) || '[]'
|
|
91
|
+
ELSE c.data_type
|
|
92
|
+
END AS data_type,
|
|
93
|
+
c.is_nullable, c.column_default, c.ordinal_position,
|
|
94
|
+
(pk.column_name IS NOT NULL) AS is_primary_key,
|
|
95
|
+
CASE WHEN fk.foreign_table IS NOT NULL
|
|
96
|
+
THEN json_build_object('table', fk.foreign_table, 'column', fk.foreign_column)
|
|
97
|
+
END AS foreign_key
|
|
98
|
+
FROM information_schema.columns c
|
|
99
|
+
LEFT JOIN pks pk
|
|
100
|
+
ON c.table_schema = pk.table_schema
|
|
101
|
+
AND c.table_name = pk.table_name
|
|
102
|
+
AND c.column_name = pk.column_name
|
|
103
|
+
LEFT JOIN fks fk
|
|
104
|
+
ON c.table_schema = fk.table_schema
|
|
105
|
+
AND c.table_name = fk.table_name
|
|
106
|
+
AND c.column_name = fk.column_name
|
|
107
|
+
WHERE c.table_schema NOT IN (
|
|
108
|
+
'pg_catalog','information_schema','pgsodium','pgsodium_masks','vault',
|
|
109
|
+
'extensions','pgbouncer','realtime','_realtime','graphql','graphql_public',
|
|
110
|
+
'supabase_functions','supabase_migrations','auth','storage','net'
|
|
111
|
+
)
|
|
112
|
+
),
|
|
113
|
+
tables_json AS (
|
|
114
|
+
SELECT
|
|
115
|
+
table_schema,
|
|
116
|
+
table_name,
|
|
117
|
+
json_agg(
|
|
118
|
+
json_build_object(
|
|
119
|
+
'name', column_name,
|
|
120
|
+
'postgresType', data_type,
|
|
121
|
+
'isNullable', is_nullable = 'YES',
|
|
122
|
+
'hasDefault', column_default IS NOT NULL,
|
|
123
|
+
'defaultExpr', column_default,
|
|
124
|
+
'isPrimaryKey', is_primary_key,
|
|
125
|
+
'foreignKey', foreign_key
|
|
126
|
+
) ORDER BY ordinal_position
|
|
127
|
+
) AS columns
|
|
128
|
+
FROM cols
|
|
129
|
+
GROUP BY table_schema, table_name
|
|
130
|
+
),
|
|
131
|
+
policies AS (
|
|
132
|
+
SELECT schemaname AS table_schema, tablename AS table_name,
|
|
133
|
+
json_agg(json_build_object(
|
|
134
|
+
'name', policyname,
|
|
135
|
+
'command', cmd,
|
|
136
|
+
'usingExpr', qual,
|
|
137
|
+
'withCheckExpr', with_check
|
|
138
|
+
)) AS policies
|
|
139
|
+
FROM pg_policies
|
|
140
|
+
GROUP BY schemaname, tablename
|
|
141
|
+
),
|
|
142
|
+
row_counts AS (
|
|
143
|
+
SELECT n.nspname AS table_schema, c.relname AS table_name,
|
|
144
|
+
GREATEST(c.reltuples, 0)::bigint AS estimated_row_count
|
|
145
|
+
FROM pg_class c
|
|
146
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
147
|
+
WHERE c.relkind = 'r'
|
|
148
|
+
),
|
|
149
|
+
tables_full AS (
|
|
150
|
+
SELECT
|
|
151
|
+
t.table_schema,
|
|
152
|
+
t.table_name,
|
|
153
|
+
t.columns,
|
|
154
|
+
COALESCE(p.policies, '[]'::json) AS rls_policies,
|
|
155
|
+
COALESCE(r.estimated_row_count, 0) AS estimated_row_count
|
|
156
|
+
FROM tables_json t
|
|
157
|
+
LEFT JOIN policies p
|
|
158
|
+
ON t.table_schema = p.table_schema AND t.table_name = p.table_name
|
|
159
|
+
LEFT JOIN row_counts r
|
|
160
|
+
ON t.table_schema = r.table_schema AND t.table_name = r.table_name
|
|
161
|
+
)
|
|
162
|
+
SELECT json_build_object(
|
|
163
|
+
'tables', COALESCE((
|
|
164
|
+
SELECT json_agg(json_build_object(
|
|
165
|
+
'schema', table_schema,
|
|
166
|
+
'name', table_name,
|
|
167
|
+
'columns', columns,
|
|
168
|
+
'rlsPolicies', rls_policies,
|
|
169
|
+
'estimatedRowCount', estimated_row_count
|
|
170
|
+
) ORDER BY table_schema, table_name) FROM tables_full
|
|
171
|
+
), '[]'::json),
|
|
172
|
+
'storageBuckets', COALESCE((
|
|
173
|
+
SELECT json_agg(json_build_object('id', id, 'name', name, 'public', public))
|
|
174
|
+
FROM storage.buckets
|
|
175
|
+
), '[]'::json),
|
|
176
|
+
'authUsersCount', (SELECT count(*)::bigint FROM auth.users)
|
|
177
|
+
) AS schema;
|
|
178
|
+
`;
|
|
179
|
+
export async function inspectSupabase(ctx, input) {
|
|
180
|
+
const url = `${SUPABASE_API_BASE_URL}/v1/projects/${encodeURIComponent(input.projectRef)}/database/query`;
|
|
181
|
+
ctx.status(`Inspecting Supabase project ${input.projectRef}...`);
|
|
182
|
+
let response;
|
|
183
|
+
try {
|
|
184
|
+
response = await fetch(url, {
|
|
185
|
+
method: "POST",
|
|
186
|
+
headers: {
|
|
187
|
+
"content-type": "application/json",
|
|
188
|
+
authorization: `Bearer ${input.token}`,
|
|
189
|
+
accept: "application/json",
|
|
190
|
+
},
|
|
191
|
+
body: JSON.stringify({ query: INTROSPECTION_SQL }),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
return failure({
|
|
196
|
+
projectRef: input.projectRef,
|
|
197
|
+
code: "network_error",
|
|
198
|
+
title: "Couldn't reach Supabase",
|
|
199
|
+
explanation: `Network error contacting ${SUPABASE_API_BASE_URL}: ${err instanceof Error ? err.message : String(err)}.`,
|
|
200
|
+
suggestion: "Check your internet connection and try again.",
|
|
201
|
+
// No `retry` recovery: the SuggestedTool union doesn't yet include
|
|
202
|
+
// `percher_inspect_supabase`, and we don't want to round-trip the
|
|
203
|
+
// Supabase PAT through `recovery.args` (where it can land in logs
|
|
204
|
+
// and telemetry). Ask the user to rerun the command — they hold
|
|
205
|
+
// the token locally and can re-issue with no recovery-channel
|
|
206
|
+
// leak.
|
|
207
|
+
recovery: recoveryAsk({
|
|
208
|
+
prompt: `Couldn't reach Supabase. Check your connection and re-run \`bunx percher migrate-from-supabase --project ${input.projectRef} --token <token>\`.`,
|
|
209
|
+
reasonCode: "infra_transient",
|
|
210
|
+
retryable: true,
|
|
211
|
+
}),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
if (response.status === 401 || response.status === 403) {
|
|
215
|
+
return failure({
|
|
216
|
+
projectRef: input.projectRef,
|
|
217
|
+
code: "auth_failed",
|
|
218
|
+
title: "Supabase token rejected",
|
|
219
|
+
explanation: "The Supabase personal access token was rejected (401/403). It may be expired, revoked, or lack the required project scope.",
|
|
220
|
+
suggestion: "Generate a fresh token at https://supabase.com/dashboard/account/tokens and pass it via --token.",
|
|
221
|
+
recovery: recoveryAsk({
|
|
222
|
+
prompt: "Your Supabase access token was rejected. Generate a new one at https://supabase.com/dashboard/account/tokens and re-run with --token <new-token>.",
|
|
223
|
+
reasonCode: "auth_required",
|
|
224
|
+
}),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
if (response.status === 404) {
|
|
228
|
+
return failure({
|
|
229
|
+
projectRef: input.projectRef,
|
|
230
|
+
code: "project_not_found",
|
|
231
|
+
title: "Supabase project not found",
|
|
232
|
+
explanation: `Project ${input.projectRef} doesn't exist or the token doesn't have access to it.`,
|
|
233
|
+
suggestion: "Check the project ref on https://supabase.com/dashboard and confirm the token belongs to the same account.",
|
|
234
|
+
recovery: recoveryAsk({
|
|
235
|
+
prompt: `Supabase project "${input.projectRef}" was not found. Verify the project ref at https://supabase.com/dashboard and re-run with the correct --project.`,
|
|
236
|
+
reasonCode: "app_not_found",
|
|
237
|
+
}),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
const rawBody = await safeReadText(response);
|
|
241
|
+
if (response.status === 408 || response.status === 504) {
|
|
242
|
+
return failure({
|
|
243
|
+
projectRef: input.projectRef,
|
|
244
|
+
code: "query_timeout",
|
|
245
|
+
title: "Schema query timed out",
|
|
246
|
+
explanation: "Supabase took too long to run the introspection query. This usually happens on very large databases.",
|
|
247
|
+
suggestion: "Re-run later, or contact Percher if it keeps timing out — we may need to chunk the introspection for huge schemas.",
|
|
248
|
+
// See network_error above for why this is ask_user not retry.
|
|
249
|
+
recovery: recoveryAsk({
|
|
250
|
+
prompt: `Supabase took too long to run the introspection query. Re-run \`bunx percher migrate-from-supabase --project ${input.projectRef} --token <token>\` in a moment, or open an issue if it keeps timing out.`,
|
|
251
|
+
reasonCode: "infra_transient",
|
|
252
|
+
retryable: true,
|
|
253
|
+
}),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
if (response.status === 503) {
|
|
257
|
+
const body = parseJsonSafe(rawBody);
|
|
258
|
+
const message = extractErrorMessage(body) ?? "Supabase project is unavailable.";
|
|
259
|
+
if (looksLikePaused(message)) {
|
|
260
|
+
return failure({
|
|
261
|
+
projectRef: input.projectRef,
|
|
262
|
+
code: "project_paused",
|
|
263
|
+
title: "Supabase project is paused",
|
|
264
|
+
explanation: "Supabase auto-pauses inactive free-tier projects. Schema introspection can't run while the project is paused.",
|
|
265
|
+
suggestion: "Unpause the project from https://supabase.com/dashboard and retry once it's running.",
|
|
266
|
+
recovery: recoveryAsk({
|
|
267
|
+
prompt: `Supabase project "${input.projectRef}" is paused. Unpause it at https://supabase.com/dashboard, wait until it's running, and re-run the same command.`,
|
|
268
|
+
reasonCode: "infra_unavailable",
|
|
269
|
+
}),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (!response.ok) {
|
|
274
|
+
return failure({
|
|
275
|
+
projectRef: input.projectRef,
|
|
276
|
+
code: "unexpected_response",
|
|
277
|
+
title: `Supabase returned ${response.status}`,
|
|
278
|
+
explanation: `Supabase Management API responded with HTTP ${response.status}: ${truncate(rawBody, 400)}`,
|
|
279
|
+
suggestion: "Inspect the response body above. If the issue is transient, retry; otherwise verify the token's scopes.",
|
|
280
|
+
recovery: recoveryAsk({
|
|
281
|
+
prompt: `Supabase Management API responded with HTTP ${response.status}. Inspect the message and decide whether to retry, switch tokens, or skip migration.`,
|
|
282
|
+
reasonCode: "unknown",
|
|
283
|
+
}),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
const parsed = parseJsonSafe(rawBody);
|
|
287
|
+
const rows = extractRows(parsed);
|
|
288
|
+
if (!rows || rows.length === 0) {
|
|
289
|
+
return failure({
|
|
290
|
+
projectRef: input.projectRef,
|
|
291
|
+
code: "unexpected_response",
|
|
292
|
+
title: "Supabase returned an empty result",
|
|
293
|
+
explanation: "The Management API accepted the query but returned no rows. This is unexpected — the introspection query always returns at least one aggregate row.",
|
|
294
|
+
suggestion: "Re-run the command; if it persists, file an issue with the project ref.",
|
|
295
|
+
// See network_error above for why this is ask_user not retry.
|
|
296
|
+
recovery: recoveryAsk({
|
|
297
|
+
prompt: `Supabase returned an empty result for the schema query — unexpected. Re-run \`bunx percher migrate-from-supabase --project ${input.projectRef} --token <token>\`; if it persists, file an issue.`,
|
|
298
|
+
reasonCode: "infra_transient",
|
|
299
|
+
retryable: true,
|
|
300
|
+
}),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
const schemaBlob = rows[0]?.schema;
|
|
304
|
+
if (!schemaBlob || typeof schemaBlob !== "object") {
|
|
305
|
+
return failure({
|
|
306
|
+
projectRef: input.projectRef,
|
|
307
|
+
code: "unexpected_response",
|
|
308
|
+
title: "Couldn't parse Supabase response",
|
|
309
|
+
explanation: `Expected a JSON object under \`schema\`; got: ${truncate(rawBody, 400)}.`,
|
|
310
|
+
suggestion: "Re-run; if it persists, file an issue with the response excerpt.",
|
|
311
|
+
recovery: recoveryAsk({
|
|
312
|
+
prompt: "Supabase returned a response the inspector couldn't parse. Try again, or open an issue with the response excerpt.",
|
|
313
|
+
reasonCode: "unknown",
|
|
314
|
+
}),
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
const tables = normalizeTables(schemaBlob.tables);
|
|
318
|
+
const storageBuckets = normalizeBuckets(schemaBlob.storageBuckets);
|
|
319
|
+
const authUsersCount = toFiniteNumber(schemaBlob.authUsersCount);
|
|
320
|
+
const warnings = collectWarnings(tables, storageBuckets, authUsersCount);
|
|
321
|
+
const summary = renderSummary({
|
|
322
|
+
projectRef: input.projectRef,
|
|
323
|
+
tables,
|
|
324
|
+
authUsersCount,
|
|
325
|
+
storageBuckets,
|
|
326
|
+
warnings,
|
|
327
|
+
});
|
|
328
|
+
return {
|
|
329
|
+
status: "ok",
|
|
330
|
+
projectRef: input.projectRef,
|
|
331
|
+
tables,
|
|
332
|
+
authUsersCount,
|
|
333
|
+
storageBuckets,
|
|
334
|
+
warnings,
|
|
335
|
+
recovery: recoveryNone(),
|
|
336
|
+
summary,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function failure(args) {
|
|
340
|
+
return {
|
|
341
|
+
status: "failed",
|
|
342
|
+
projectRef: args.projectRef,
|
|
343
|
+
tables: [],
|
|
344
|
+
authUsersCount: null,
|
|
345
|
+
storageBuckets: [],
|
|
346
|
+
warnings: [],
|
|
347
|
+
error: {
|
|
348
|
+
code: args.code,
|
|
349
|
+
title: args.title,
|
|
350
|
+
explanation: args.explanation,
|
|
351
|
+
suggestion: args.suggestion,
|
|
352
|
+
},
|
|
353
|
+
recovery: args.recovery,
|
|
354
|
+
summary: `${args.title}. ${args.suggestion}`,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
async function safeReadText(response) {
|
|
358
|
+
try {
|
|
359
|
+
return await response.text();
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
return "";
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function parseJsonSafe(text) {
|
|
366
|
+
if (!text)
|
|
367
|
+
return null;
|
|
368
|
+
try {
|
|
369
|
+
return JSON.parse(text);
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function extractRows(body) {
|
|
376
|
+
if (Array.isArray(body))
|
|
377
|
+
return body;
|
|
378
|
+
if (body && typeof body === "object") {
|
|
379
|
+
const maybeRows = body.result ?? body.rows;
|
|
380
|
+
if (Array.isArray(maybeRows))
|
|
381
|
+
return maybeRows;
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
function extractErrorMessage(body) {
|
|
386
|
+
if (!body || typeof body !== "object")
|
|
387
|
+
return null;
|
|
388
|
+
const candidate = body.message ??
|
|
389
|
+
body.error?.message ??
|
|
390
|
+
body.msg;
|
|
391
|
+
return typeof candidate === "string" ? candidate : null;
|
|
392
|
+
}
|
|
393
|
+
function looksLikePaused(message) {
|
|
394
|
+
const lower = message.toLowerCase();
|
|
395
|
+
return lower.includes("paused") || lower.includes("inactive");
|
|
396
|
+
}
|
|
397
|
+
function normalizeTables(input) {
|
|
398
|
+
if (!Array.isArray(input))
|
|
399
|
+
return [];
|
|
400
|
+
const tables = [];
|
|
401
|
+
for (const raw of input) {
|
|
402
|
+
if (!raw || typeof raw !== "object")
|
|
403
|
+
continue;
|
|
404
|
+
const r = raw;
|
|
405
|
+
const schema = typeof r.schema === "string" ? r.schema : "";
|
|
406
|
+
const name = typeof r.name === "string" ? r.name : "";
|
|
407
|
+
if (!schema || !name)
|
|
408
|
+
continue;
|
|
409
|
+
if (SYSTEM_SCHEMAS.has(schema))
|
|
410
|
+
continue;
|
|
411
|
+
tables.push({
|
|
412
|
+
schema,
|
|
413
|
+
name,
|
|
414
|
+
columns: normalizeColumns(r.columns),
|
|
415
|
+
rlsPolicies: normalizeRlsPolicies(r.rlsPolicies),
|
|
416
|
+
estimatedRowCount: toFiniteNumber(r.estimatedRowCount) ?? 0,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
return tables;
|
|
420
|
+
}
|
|
421
|
+
function normalizeColumns(input) {
|
|
422
|
+
if (!Array.isArray(input))
|
|
423
|
+
return [];
|
|
424
|
+
const cols = [];
|
|
425
|
+
for (const raw of input) {
|
|
426
|
+
if (!raw || typeof raw !== "object")
|
|
427
|
+
continue;
|
|
428
|
+
const r = raw;
|
|
429
|
+
const name = typeof r.name === "string" ? r.name : "";
|
|
430
|
+
const postgresType = typeof r.postgresType === "string" ? r.postgresType : "";
|
|
431
|
+
if (!name || !postgresType)
|
|
432
|
+
continue;
|
|
433
|
+
cols.push({
|
|
434
|
+
name,
|
|
435
|
+
postgresType,
|
|
436
|
+
isNullable: r.isNullable === true,
|
|
437
|
+
hasDefault: r.hasDefault === true,
|
|
438
|
+
defaultExpr: typeof r.defaultExpr === "string" ? r.defaultExpr : null,
|
|
439
|
+
isPrimaryKey: r.isPrimaryKey === true,
|
|
440
|
+
foreignKey: normalizeForeignKey(r.foreignKey),
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
return cols;
|
|
444
|
+
}
|
|
445
|
+
function normalizeForeignKey(input) {
|
|
446
|
+
if (!input || typeof input !== "object")
|
|
447
|
+
return null;
|
|
448
|
+
const r = input;
|
|
449
|
+
const table = typeof r.table === "string" ? r.table : "";
|
|
450
|
+
const column = typeof r.column === "string" ? r.column : "";
|
|
451
|
+
if (!table || !column)
|
|
452
|
+
return null;
|
|
453
|
+
return { table, column };
|
|
454
|
+
}
|
|
455
|
+
function normalizeRlsPolicies(input) {
|
|
456
|
+
if (!Array.isArray(input))
|
|
457
|
+
return [];
|
|
458
|
+
const policies = [];
|
|
459
|
+
for (const raw of input) {
|
|
460
|
+
if (!raw || typeof raw !== "object")
|
|
461
|
+
continue;
|
|
462
|
+
const r = raw;
|
|
463
|
+
const name = typeof r.name === "string" ? r.name : "";
|
|
464
|
+
const command = typeof r.command === "string" ? r.command : "";
|
|
465
|
+
if (!name)
|
|
466
|
+
continue;
|
|
467
|
+
policies.push({
|
|
468
|
+
name,
|
|
469
|
+
command,
|
|
470
|
+
usingExpr: typeof r.usingExpr === "string" ? r.usingExpr : null,
|
|
471
|
+
withCheckExpr: typeof r.withCheckExpr === "string" ? r.withCheckExpr : null,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
return policies;
|
|
475
|
+
}
|
|
476
|
+
function normalizeBuckets(input) {
|
|
477
|
+
if (!Array.isArray(input))
|
|
478
|
+
return [];
|
|
479
|
+
const buckets = [];
|
|
480
|
+
for (const raw of input) {
|
|
481
|
+
if (!raw || typeof raw !== "object")
|
|
482
|
+
continue;
|
|
483
|
+
const r = raw;
|
|
484
|
+
const id = typeof r.id === "string" ? r.id : "";
|
|
485
|
+
const name = typeof r.name === "string" ? r.name : "";
|
|
486
|
+
if (!id || !name)
|
|
487
|
+
continue;
|
|
488
|
+
buckets.push({ id, name, public: r.public === true });
|
|
489
|
+
}
|
|
490
|
+
return buckets;
|
|
491
|
+
}
|
|
492
|
+
function toFiniteNumber(input) {
|
|
493
|
+
if (typeof input === "number" && Number.isFinite(input))
|
|
494
|
+
return input;
|
|
495
|
+
if (typeof input === "string") {
|
|
496
|
+
const n = Number(input);
|
|
497
|
+
return Number.isFinite(n) ? n : null;
|
|
498
|
+
}
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
function truncate(s, max) {
|
|
502
|
+
if (s.length <= max)
|
|
503
|
+
return s;
|
|
504
|
+
return `${s.slice(0, max)}…`;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* True for any Postgres array column. Accepts both the formatted
|
|
508
|
+
* `text[]` shape that our SQL emits AND the raw `ARRAY` string that
|
|
509
|
+
* information_schema reports without formatting. The second branch is
|
|
510
|
+
* the defensive one — if Supabase ever changes their query envelope or
|
|
511
|
+
* a regression strips the SQL's array-formatting CASE, the warning
|
|
512
|
+
* still fires instead of going silent.
|
|
513
|
+
*/
|
|
514
|
+
function isArrayType(postgresType) {
|
|
515
|
+
return postgresType === "ARRAY" || postgresType.endsWith("[]");
|
|
516
|
+
}
|
|
517
|
+
function collectWarnings(tables, buckets, authUsersCount) {
|
|
518
|
+
const warnings = [];
|
|
519
|
+
for (const table of tables) {
|
|
520
|
+
const pkCount = table.columns.filter((c) => c.isPrimaryKey).length;
|
|
521
|
+
if (pkCount === 0) {
|
|
522
|
+
warnings.push(`Table "${table.schema}.${table.name}" has no primary key — PocketBase requires one. A synthetic id will be generated.`);
|
|
523
|
+
}
|
|
524
|
+
else if (pkCount > 1) {
|
|
525
|
+
warnings.push(`Table "${table.schema}.${table.name}" has a composite primary key (${pkCount} columns) — PocketBase only supports a single id; needs manual review.`);
|
|
526
|
+
}
|
|
527
|
+
for (const col of table.columns) {
|
|
528
|
+
// The SQL collapses information_schema's `ARRAY` data_type +
|
|
529
|
+
// underscore-prefixed udt_name into the ANSI `text[]` syntax.
|
|
530
|
+
// Defensive: also treat the raw `ARRAY` literal as an array — if a
|
|
531
|
+
// SQL regression ever leaks the unformatted form through, the
|
|
532
|
+
// warning still fires instead of going silent. Codex P2 finding
|
|
533
|
+
// pinned this with a test below.
|
|
534
|
+
if (isArrayType(col.postgresType)) {
|
|
535
|
+
warnings.push(`Column "${table.schema}.${table.name}.${col.name}" is an array (${col.postgresType}) — PocketBase has no array UI; will map to json.`);
|
|
536
|
+
}
|
|
537
|
+
if (col.postgresType === "USER-DEFINED") {
|
|
538
|
+
warnings.push(`Column "${table.schema}.${table.name}.${col.name}" uses a user-defined type (likely enum or custom) — verify the PocketBase mapping manually.`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (table.rlsPolicies.length > 0) {
|
|
542
|
+
warnings.push(`Table "${table.schema}.${table.name}" has ${table.rlsPolicies.length} RLS polic${table.rlsPolicies.length === 1 ? "y" : "ies"} — PocketBase API rules use different syntax; phase D will report each policy for manual rewrite.`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (buckets.length > 0) {
|
|
546
|
+
warnings.push(`${buckets.length} storage bucket${buckets.length === 1 ? "" : "s"} found — bucket files need a separate storage migration script (phase C).`);
|
|
547
|
+
}
|
|
548
|
+
if (authUsersCount !== null && authUsersCount > 0) {
|
|
549
|
+
warnings.push(`${authUsersCount} auth user${authUsersCount === 1 ? "" : "s"} found — Supabase bcrypt hashes are NOT directly compatible with PocketBase; users will need to reset their password.`);
|
|
550
|
+
}
|
|
551
|
+
return warnings;
|
|
552
|
+
}
|
|
553
|
+
function renderSummary(args) {
|
|
554
|
+
const columnCount = args.tables.reduce((acc, t) => acc + t.columns.length, 0);
|
|
555
|
+
const policyCount = args.tables.reduce((acc, t) => acc + t.rlsPolicies.length, 0);
|
|
556
|
+
const parts = [
|
|
557
|
+
`Inspected Supabase project ${args.projectRef}:`,
|
|
558
|
+
`${args.tables.length} table${args.tables.length === 1 ? "" : "s"} (${columnCount} columns)`,
|
|
559
|
+
`${policyCount} RLS polic${policyCount === 1 ? "y" : "ies"}`,
|
|
560
|
+
`${args.storageBuckets.length} storage bucket${args.storageBuckets.length === 1 ? "" : "s"}`,
|
|
561
|
+
args.authUsersCount === null
|
|
562
|
+
? "auth.users unavailable"
|
|
563
|
+
: `${args.authUsersCount} auth user${args.authUsersCount === 1 ? "" : "s"}`,
|
|
564
|
+
];
|
|
565
|
+
const head = `${parts[0]} ${parts.slice(1).join(", ")}.`;
|
|
566
|
+
if (args.warnings.length === 0)
|
|
567
|
+
return head;
|
|
568
|
+
return `${head} ${args.warnings.length} warning${args.warnings.length === 1 ? "" : "s"} — see warnings[] for details.`;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Helper for downstream code (phases B–D) to filter tables to just the
|
|
572
|
+
* ones we'd migrate — i.e. nothing in a Supabase-managed system schema.
|
|
573
|
+
* Phase A returns these already filtered, but the predicate is exported
|
|
574
|
+
* so a caller can re-filter a result they've stored.
|
|
575
|
+
*/
|
|
576
|
+
export function isMigratableTable(table) {
|
|
577
|
+
return !SYSTEM_SCHEMAS.has(table.schema);
|
|
578
|
+
}
|
|
579
|
+
//# sourceMappingURL=migrate-supabase.js.map
|