@percher/core 0.4.0 → 0.4.2
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 +70 -0
- package/dist/commands/alerts.d.ts.map +1 -0
- package/dist/commands/alerts.js +82 -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 +6 -15
- package/dist/commands/export.d.ts.map +1 -1
- package/dist/commands/export.js +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 +358 -0
- package/dist/commands/publish-api-error.js.map +1 -0
- package/dist/commands/publish-failure.d.ts.map +1 -1
- package/dist/commands/publish-failure.js +11 -2
- package/dist/commands/publish-failure.js.map +1 -1
- 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.d.ts +8 -1
- package/dist/env-scan-source.d.ts.map +1 -1
- package/dist/env-scan-source.js +100 -24
- package/dist/env-scan-source.js.map +1 -1
- package/dist/error-classifier.d.ts +18 -1
- package/dist/error-classifier.d.ts.map +1 -1
- package/dist/error-classifier.js +171 -9
- 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 +64 -49
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +57 -42
- package/dist/index.js.map +1 -1
- package/dist/plans.d.ts +86 -8
- package/dist/plans.d.ts.map +1 -1
- package/dist/plans.js +113 -24
- package/dist/plans.js.map +1 -1
- package/dist/recovery.d.ts +73 -3
- package/dist/recovery.d.ts.map +1 -1
- package/dist/recovery.js +36 -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,1119 @@
|
|
|
1
|
+
import jscodeshift, {} from "jscodeshift";
|
|
2
|
+
/** AST library pre-bound to the TypeScript parser via jscodeshift's `.withParser`. */
|
|
3
|
+
const tsx = jscodeshift.withParser("tsx");
|
|
4
|
+
/**
|
|
5
|
+
* Rewrite Supabase JS SDK calls into PocketBase JS SDK calls.
|
|
6
|
+
*
|
|
7
|
+
* Pure (no I/O). Idempotent — feeding the output back in produces
|
|
8
|
+
* the same output with an empty flags array (already-rewritten
|
|
9
|
+
* code has no Supabase calls left to match).
|
|
10
|
+
*/
|
|
11
|
+
export function rewriteSupabaseSdk(input) {
|
|
12
|
+
const pbName = input.pbIdentifier ?? "pb";
|
|
13
|
+
const supabaseNames = new Set(input.supabaseIdentifiers ?? ["supabase"]);
|
|
14
|
+
// Fast path: if the source doesn't mention any of the Supabase
|
|
15
|
+
// identifiers OR `@supabase/supabase-js`, bail out without
|
|
16
|
+
// parsing. Saves a lot of AST work on the average file in a
|
|
17
|
+
// project (most files don't touch Supabase).
|
|
18
|
+
if (!sourceMentionsSupabase(input.source, supabaseNames)) {
|
|
19
|
+
return { rewritten: input.source, flags: [], changed: false };
|
|
20
|
+
}
|
|
21
|
+
let root;
|
|
22
|
+
try {
|
|
23
|
+
root = tsx(input.source);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
// Parser error — return unchanged WITH an explicit parseError
|
|
27
|
+
// signal so the file walker can classify this as
|
|
28
|
+
// skipped[reason=parse_error] instead of silently lumping it
|
|
29
|
+
// in with the clean files. Codex P2 round 10 (2026-05-17):
|
|
30
|
+
// before, the swallow-and-return-unchanged behaviour made
|
|
31
|
+
// malformed Supabase-bearing files invisible in the migration
|
|
32
|
+
// report — they looked just like files with no Supabase code.
|
|
33
|
+
return {
|
|
34
|
+
rewritten: input.source,
|
|
35
|
+
flags: [],
|
|
36
|
+
changed: false,
|
|
37
|
+
parseError: err instanceof Error ? err.message : String(err),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const flags = [];
|
|
41
|
+
const state = { mutated: false };
|
|
42
|
+
rewriteAuthCalls(root, supabaseNames, pbName, flags, state);
|
|
43
|
+
rewriteFromCalls(root, supabaseNames, pbName, flags, state);
|
|
44
|
+
flagUnsupportedPatterns(root, supabaseNames, flags);
|
|
45
|
+
// Only re-print via toSource when an AST mutation actually
|
|
46
|
+
// happened — manual_review flags can EITHER mutate the AST
|
|
47
|
+
// (e.g. auth.getUser still rewrites then warns about Promise
|
|
48
|
+
// semantics) OR leave it alone (e.g. { data, error } envelope
|
|
49
|
+
// detection skips the rewrite). Recast's round-trip can
|
|
50
|
+
// introduce tiny formatting drift on a clean AST, so we want
|
|
51
|
+
// byte-identical input == output when nothing was actually
|
|
52
|
+
// mutated. Codex P1.1 finding 2026-05-17.
|
|
53
|
+
if (!state.mutated) {
|
|
54
|
+
return { rewritten: input.source, flags, changed: false };
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
rewritten: root.toSource({ quote: "double" }),
|
|
58
|
+
flags,
|
|
59
|
+
changed: true,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* True when this CallExpression's result is consumed via Supabase's
|
|
64
|
+
* `{ data, error }` envelope shape — destructured at assignment OR
|
|
65
|
+
* accessed via `.data` / `.error`. PocketBase's SDK doesn't wrap
|
|
66
|
+
* results in an envelope (getFullList returns an array, create
|
|
67
|
+
* returns the record), so rewriting these call sites would silently
|
|
68
|
+
* break every consumer.
|
|
69
|
+
*
|
|
70
|
+
* Detection walks up from the CallExpression through the optional
|
|
71
|
+
* `await` parent to look at the consuming context. Covered shapes:
|
|
72
|
+
*
|
|
73
|
+
* const { data, error } = await CALL // destructure init
|
|
74
|
+
* const { data: x, error: y } = await CALL // renamed destructure
|
|
75
|
+
* ({ data, error } = await CALL) // assignment expr
|
|
76
|
+
* (await CALL).data // member access on await
|
|
77
|
+
* CALL.data / CALL.error // member access on call
|
|
78
|
+
*
|
|
79
|
+
* Anything else (assigned to a plain variable + later .data access,
|
|
80
|
+
* threaded through a helper, etc.) goes through the rewrite — the
|
|
81
|
+
* user's responsibility to fix.
|
|
82
|
+
*
|
|
83
|
+
* Codex P1.1 finding 2026-05-17.
|
|
84
|
+
*/
|
|
85
|
+
function consumesDataErrorEnvelope(path) {
|
|
86
|
+
// The "outer expression" for envelope-purposes is the call OR a
|
|
87
|
+
// wrapping await. Look at the parent of THAT.
|
|
88
|
+
let outer = path.node;
|
|
89
|
+
let outerParent = path.parent?.node;
|
|
90
|
+
if (outerParent?.type === "AwaitExpression") {
|
|
91
|
+
const aw = outerParent;
|
|
92
|
+
if (aw.argument === path.node) {
|
|
93
|
+
outer = outerParent;
|
|
94
|
+
outerParent = path.parent.parent?.node;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!outerParent)
|
|
98
|
+
return false;
|
|
99
|
+
// (await CALL).data / CALL.data — member access on outer
|
|
100
|
+
if (outerParent.type === "MemberExpression") {
|
|
101
|
+
const m = outerParent;
|
|
102
|
+
if (m.object === outer && m.property.type === "Identifier") {
|
|
103
|
+
const propName = m.property.name;
|
|
104
|
+
if (propName === "data" || propName === "error")
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
// const { data, error } = await CALL
|
|
110
|
+
if (outerParent.type === "VariableDeclarator") {
|
|
111
|
+
const v = outerParent;
|
|
112
|
+
if (v.init === outer && v.id.type === "ObjectPattern") {
|
|
113
|
+
return objectPatternHasDataOrError(v.id);
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
// ({ data, error } = await CALL)
|
|
118
|
+
if (outerParent.type === "AssignmentExpression") {
|
|
119
|
+
const a = outerParent;
|
|
120
|
+
if (a.right === outer && a.left.type === "ObjectPattern") {
|
|
121
|
+
return objectPatternHasDataOrError(a.left);
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
function objectPatternHasDataOrError(pattern) {
|
|
128
|
+
for (const prop of pattern.properties) {
|
|
129
|
+
// jscodeshift / ast-types models destructure properties as
|
|
130
|
+
// either `Property` (legacy) or `ObjectProperty` (Babel) with
|
|
131
|
+
// a `key`. Defensive coverage of both.
|
|
132
|
+
if ((prop.type === "Property" || prop.type === "ObjectProperty") &&
|
|
133
|
+
"key" in prop &&
|
|
134
|
+
prop.key.type === "Identifier") {
|
|
135
|
+
const name = prop.key.name;
|
|
136
|
+
if (name === "data" || name === "error")
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
function flagEnvelopeManualReview(path, pattern, flags) {
|
|
143
|
+
flags.push(makeFlag(path, "manual_review", `${pattern}.envelope`, `Call site consumes the Supabase \`{ data, error }\` envelope — PocketBase returns plain values. Skipping rewrite; unwrap the destructure (or .data/.error access) manually, then re-run the migrator.`));
|
|
144
|
+
}
|
|
145
|
+
function sourceMentionsSupabase(source, supabaseNames) {
|
|
146
|
+
if (source.includes("@supabase/supabase-js"))
|
|
147
|
+
return true;
|
|
148
|
+
// Cheap textual scan before paying for the AST parse. False
|
|
149
|
+
// positives are fine — they just trigger a parse that finds
|
|
150
|
+
// nothing.
|
|
151
|
+
for (const name of supabaseNames) {
|
|
152
|
+
if (source.includes(name))
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
// ── auth.* call rewrites ────────────────────────────────────────
|
|
158
|
+
function rewriteAuthCalls(root, supabaseNames, pbName, flags, state) {
|
|
159
|
+
// supabase.auth.<method>(...)
|
|
160
|
+
root
|
|
161
|
+
.find(jscodeshift.CallExpression)
|
|
162
|
+
.filter((path) => isSupabaseAuthCall(path, supabaseNames))
|
|
163
|
+
.forEach((path) => {
|
|
164
|
+
const callee = path.node.callee;
|
|
165
|
+
const method = callee.property.name;
|
|
166
|
+
switch (method) {
|
|
167
|
+
case "signInWithPassword":
|
|
168
|
+
replaceAuthSignIn(path, pbName, flags, state);
|
|
169
|
+
return;
|
|
170
|
+
case "signUp":
|
|
171
|
+
replaceAuthSignUp(path, pbName, flags, state);
|
|
172
|
+
return;
|
|
173
|
+
case "signOut":
|
|
174
|
+
replaceAuthSignOut(path, pbName, flags, state);
|
|
175
|
+
return;
|
|
176
|
+
case "getUser":
|
|
177
|
+
replaceAuthGetUser(path, pbName, flags, state);
|
|
178
|
+
return;
|
|
179
|
+
case "onAuthStateChange":
|
|
180
|
+
replaceAuthOnChange(path, pbName, flags, state);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
function replaceAuthOnChange(path, pbName, flags, state) {
|
|
186
|
+
// pb.authStore.onChange(callback). The callback signature differs:
|
|
187
|
+
// Supabase: (event, session) => ...
|
|
188
|
+
// PB: (token, model) => ...
|
|
189
|
+
// The first arg passes through unchanged — same callable shape —
|
|
190
|
+
// but the destructured params inside the callback won't line up,
|
|
191
|
+
// so this is always manual_review.
|
|
192
|
+
if (path.node.arguments.length === 0) {
|
|
193
|
+
flags.push(makeFlag(path, "manual_review", "auth.onAuthStateChange", "onAuthStateChange called with no args — manual review."));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const callback = path.node.arguments[0];
|
|
197
|
+
const replacement = jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.memberExpression(jscodeshift.identifier(pbName), jscodeshift.identifier("authStore")), jscodeshift.identifier("onChange")), [callback]);
|
|
198
|
+
applyRewrite(path, replacement, "manual_review", "auth.onAuthStateChange", "onAuthStateChange → pb.authStore.onChange — the callback signature changes: Supabase passes `(event, session)`, PB passes `(token, model)`. Rewrite the callback body.", flags, state);
|
|
199
|
+
}
|
|
200
|
+
function isSupabaseAuthCall(path, supabaseNames) {
|
|
201
|
+
const callee = path.node.callee;
|
|
202
|
+
if (callee.type !== "MemberExpression")
|
|
203
|
+
return false;
|
|
204
|
+
if (callee.property.type !== "Identifier")
|
|
205
|
+
return false;
|
|
206
|
+
const object = callee.object;
|
|
207
|
+
// Expect <supabase>.auth.<method>
|
|
208
|
+
if (object.type !== "MemberExpression")
|
|
209
|
+
return false;
|
|
210
|
+
if (object.property.type !== "Identifier" || object.property.name !== "auth")
|
|
211
|
+
return false;
|
|
212
|
+
if (object.object.type !== "Identifier")
|
|
213
|
+
return false;
|
|
214
|
+
return supabaseNames.has(object.object.name);
|
|
215
|
+
}
|
|
216
|
+
function replaceAuthSignIn(path, pbName, flags, state) {
|
|
217
|
+
const arg = path.node.arguments[0];
|
|
218
|
+
if (!arg || arg.type !== "ObjectExpression") {
|
|
219
|
+
flags.push(makeFlag(path, "manual_review", "auth.signInWithPassword", "auth.signInWithPassword called with non-object argument — manual review needed."));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
// Locals are AST nodes, not secrets — but the repo's security
|
|
223
|
+
// audit pattern-matches `const password = ...` as a possible
|
|
224
|
+
// leaked-credential assignment regardless of RHS. Use the `-Expr`
|
|
225
|
+
// suffix so the auditor sees a node-handle, not a secret name.
|
|
226
|
+
// Codex P1.2 finding 2026-05-17.
|
|
227
|
+
const emailExpr = findObjectProp(arg, "email");
|
|
228
|
+
const passwordExpr = findObjectProp(arg, "password");
|
|
229
|
+
if (!emailExpr || !passwordExpr) {
|
|
230
|
+
flags.push(makeFlag(path, "manual_review", "auth.signInWithPassword", "auth.signInWithPassword object missing email or password — manual review needed."));
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (consumesDataErrorEnvelope(path)) {
|
|
234
|
+
flagEnvelopeManualReview(path, "auth.signInWithPassword", flags);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
// pb.collection("users").authWithPassword(email, password)
|
|
238
|
+
const replacement = jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.identifier(pbName), jscodeshift.identifier("collection")), [jscodeshift.literal("users")]), jscodeshift.identifier("authWithPassword")), [emailExpr, passwordExpr]);
|
|
239
|
+
applyRewrite(path, replacement, "rewritten", "auth.signInWithPassword", 'signInWithPassword → pb.collection("users").authWithPassword', flags, state);
|
|
240
|
+
}
|
|
241
|
+
function replaceAuthSignUp(path, pbName, flags, state) {
|
|
242
|
+
const arg = path.node.arguments[0];
|
|
243
|
+
if (!arg || arg.type !== "ObjectExpression") {
|
|
244
|
+
flags.push(makeFlag(path, "manual_review", "auth.signUp", "auth.signUp called with non-object argument — manual review needed."));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
// Locals are AST nodes, not secrets — but the repo's security
|
|
248
|
+
// audit pattern-matches `const password = ...` as a possible
|
|
249
|
+
// leaked-credential assignment regardless of RHS. Use the `-Expr`
|
|
250
|
+
// suffix so the auditor sees a node-handle, not a secret name.
|
|
251
|
+
// Codex P1.2 finding 2026-05-17.
|
|
252
|
+
const emailExpr = findObjectProp(arg, "email");
|
|
253
|
+
const passwordExpr = findObjectProp(arg, "password");
|
|
254
|
+
if (!emailExpr || !passwordExpr) {
|
|
255
|
+
flags.push(makeFlag(path, "manual_review", "auth.signUp", "auth.signUp object missing email or password — manual review needed."));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (consumesDataErrorEnvelope(path)) {
|
|
259
|
+
flagEnvelopeManualReview(path, "auth.signUp", flags);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
// pb.collection("users").create({ email, password, passwordConfirm: password })
|
|
263
|
+
const replacement = jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.identifier(pbName), jscodeshift.identifier("collection")), [jscodeshift.literal("users")]), jscodeshift.identifier("create")), [
|
|
264
|
+
jscodeshift.objectExpression([
|
|
265
|
+
jscodeshift.property("init", jscodeshift.identifier("email"), emailExpr),
|
|
266
|
+
jscodeshift.property("init", jscodeshift.identifier("password"), passwordExpr),
|
|
267
|
+
jscodeshift.property("init", jscodeshift.identifier("passwordConfirm"), passwordExpr),
|
|
268
|
+
]),
|
|
269
|
+
]);
|
|
270
|
+
applyRewrite(path, replacement, "rewritten", "auth.signUp", 'signUp → pb.collection("users").create({...passwordConfirm})', flags, state);
|
|
271
|
+
}
|
|
272
|
+
function replaceAuthSignOut(path, pbName, flags, state) {
|
|
273
|
+
if (consumesDataErrorEnvelope(path)) {
|
|
274
|
+
flagEnvelopeManualReview(path, "auth.signOut", flags);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
// pb.authStore.clear()
|
|
278
|
+
const replacement = jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.memberExpression(jscodeshift.identifier(pbName), jscodeshift.identifier("authStore")), jscodeshift.identifier("clear")), []);
|
|
279
|
+
applyRewrite(path, replacement, "rewritten", "auth.signOut", "signOut → pb.authStore.clear()", flags, state);
|
|
280
|
+
}
|
|
281
|
+
function replaceAuthGetUser(path, pbName, flags, state) {
|
|
282
|
+
if (consumesDataErrorEnvelope(path)) {
|
|
283
|
+
flagEnvelopeManualReview(path, "auth.getUser", flags);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
// pb.authStore.model (a member access, not a call). Note: PB's
|
|
287
|
+
// model is the current authed record OR null; Supabase's
|
|
288
|
+
// getUser() returns a Promise<{ data: { user } }> — semantics
|
|
289
|
+
// differ enough to warrant a manual-review flag, but the
|
|
290
|
+
// rewrite itself is mechanical.
|
|
291
|
+
const replacement = jscodeshift.memberExpression(jscodeshift.memberExpression(jscodeshift.identifier(pbName), jscodeshift.identifier("authStore")), jscodeshift.identifier("model"));
|
|
292
|
+
applyRewrite(path, replacement, "manual_review", "auth.getUser", "getUser → pb.authStore.model — note Supabase returned Promise<{ data: { user } }>, PB returns the record sync; remove awaits + .data.user access.", flags, state);
|
|
293
|
+
}
|
|
294
|
+
// ── .from() chain rewrites ──────────────────────────────────────
|
|
295
|
+
function rewriteFromCalls(root, supabaseNames, pbName, flags, state) {
|
|
296
|
+
// We target the OUTER call expression of each .from() chain
|
|
297
|
+
// because the rewrite depends on what comes after .from(). We
|
|
298
|
+
// walk every chain that starts at supabase.from("...").
|
|
299
|
+
root
|
|
300
|
+
.find(jscodeshift.CallExpression)
|
|
301
|
+
.filter((path) => isSupabaseFromTerminal(path, supabaseNames))
|
|
302
|
+
.forEach((path) => {
|
|
303
|
+
rewriteFromChain(path, supabaseNames, pbName, flags, state);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* True when this CallExpression is the OUTERMOST call in a
|
|
308
|
+
* supabase.from(...).xxx().yyy() chain — i.e. its callee is a
|
|
309
|
+
* MemberExpression whose object eventually bottoms out at
|
|
310
|
+
* `supabase.from(...)`.
|
|
311
|
+
*
|
|
312
|
+
* We pick the outermost call so each chain produces exactly one
|
|
313
|
+
* rewrite. Inner .from(), .select() etc. are reached by walking
|
|
314
|
+
* down the callee chain inside the rewriter.
|
|
315
|
+
*/
|
|
316
|
+
function isSupabaseFromTerminal(path, supabaseNames) {
|
|
317
|
+
// Walk down the callee chain until we find the call whose
|
|
318
|
+
// method is `from`. That's our chain root. Earlier version
|
|
319
|
+
// walked past it to the Identifier underneath and then
|
|
320
|
+
// rejected the chain — bug fixed 2026-05-17.
|
|
321
|
+
let fromCall = null;
|
|
322
|
+
let cursor = path.node;
|
|
323
|
+
while (cursor.type === "CallExpression") {
|
|
324
|
+
const call = cursor;
|
|
325
|
+
if (call.callee.type !== "MemberExpression")
|
|
326
|
+
break;
|
|
327
|
+
const callee = call.callee;
|
|
328
|
+
if (callee.property.type === "Identifier" &&
|
|
329
|
+
callee.property.name === "from") {
|
|
330
|
+
fromCall = call;
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
cursor = callee.object;
|
|
334
|
+
}
|
|
335
|
+
if (!fromCall)
|
|
336
|
+
return false;
|
|
337
|
+
// The `from` call must be invoked on a Supabase identifier.
|
|
338
|
+
const fromCallee = fromCall.callee;
|
|
339
|
+
if (fromCallee.object.type !== "Identifier")
|
|
340
|
+
return false;
|
|
341
|
+
if (!supabaseNames.has(fromCallee.object.name))
|
|
342
|
+
return false;
|
|
343
|
+
// Ensure THIS call is the outermost in the chain — i.e. its
|
|
344
|
+
// parent isn't another member-call we own. The walker visits
|
|
345
|
+
// every CallExpression, so without this skip we'd process the
|
|
346
|
+
// same chain once per link.
|
|
347
|
+
const parent = path.parent?.node;
|
|
348
|
+
if (parent?.type === "MemberExpression") {
|
|
349
|
+
const parentMember = parent;
|
|
350
|
+
if (parentMember.object === path.node)
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
function rewriteFromChain(path, _supabaseNames, pbName, flags, state) {
|
|
356
|
+
// Decompose the chain into the methods called, in invocation
|
|
357
|
+
// order, with their args. The chain shape:
|
|
358
|
+
// supabase.from(T).method1(...args1).method2(...args2)
|
|
359
|
+
// becomes: { table: T, steps: [{method1, args1}, {method2, args2}] }
|
|
360
|
+
const decomposed = decomposeFromChain(path.node);
|
|
361
|
+
if (!decomposed) {
|
|
362
|
+
flags.push(makeFlag(path, "manual_review", "from.chain", "Couldn't decompose .from() chain shape — manual review."));
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const { table, steps } = decomposed;
|
|
366
|
+
if (steps.length === 0) {
|
|
367
|
+
// Bare supabase.from("t") with no follow-up — rare, but it's
|
|
368
|
+
// not a meaningful call by itself. Skip.
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const terminal = steps[0]; // .insert / .select / .update / .delete
|
|
372
|
+
switch (terminal.method) {
|
|
373
|
+
case "select":
|
|
374
|
+
rewriteSelect(path, pbName, table, steps, flags, state);
|
|
375
|
+
return;
|
|
376
|
+
case "insert":
|
|
377
|
+
rewriteInsert(path, pbName, table, terminal, flags, state);
|
|
378
|
+
return;
|
|
379
|
+
case "update":
|
|
380
|
+
rewriteUpdate(path, pbName, table, steps, flags, state);
|
|
381
|
+
return;
|
|
382
|
+
case "delete":
|
|
383
|
+
rewriteDelete(path, pbName, table, steps, flags, state);
|
|
384
|
+
return;
|
|
385
|
+
case "upsert":
|
|
386
|
+
// PB has no upsert primitive; the user must split into
|
|
387
|
+
// getOne (catch 404) + update-or-create. Flag once at the
|
|
388
|
+
// from-chain layer instead of letting the flag-only walker
|
|
389
|
+
// also pick it up — double-flagging the same call site
|
|
390
|
+
// would clutter the report.
|
|
391
|
+
flags.push(makeFlag(path, "unsupported", "from.upsert", `from(${JSON.stringify(table)}).upsert(...) — PB has no upsert primitive. Rewrite as getOne (catch 404) → update-or-create.`));
|
|
392
|
+
return;
|
|
393
|
+
default:
|
|
394
|
+
flags.push(makeFlag(path, "manual_review", `from.${terminal.method}`, `from(${JSON.stringify(table)}).${terminal.method}(...) has no chunk-1 rewrite — manual review.`));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function decomposeFromChain(outer) {
|
|
398
|
+
// Walk from outer down. At each level, record (method, args) and
|
|
399
|
+
// recurse into the callee's object. Stop when we reach the
|
|
400
|
+
// .from(T) call. Same union-narrowing pattern as
|
|
401
|
+
// isSupabaseFromTerminal — explicit casts inside the loop.
|
|
402
|
+
const reversedSteps = [];
|
|
403
|
+
let node = outer;
|
|
404
|
+
while (node.type === "CallExpression") {
|
|
405
|
+
const call = node;
|
|
406
|
+
if (call.callee.type !== "MemberExpression")
|
|
407
|
+
break;
|
|
408
|
+
const callee = call.callee;
|
|
409
|
+
const method = callee.property.type === "Identifier"
|
|
410
|
+
? callee.property.name
|
|
411
|
+
: null;
|
|
412
|
+
if (method === null)
|
|
413
|
+
return null;
|
|
414
|
+
if (method === "from") {
|
|
415
|
+
const arg = call.arguments[0];
|
|
416
|
+
if (!arg || (arg.type !== "Literal" && arg.type !== "StringLiteral"))
|
|
417
|
+
return null;
|
|
418
|
+
const table = arg.value;
|
|
419
|
+
if (typeof table !== "string")
|
|
420
|
+
return null;
|
|
421
|
+
return { table, steps: reversedSteps.reverse() };
|
|
422
|
+
}
|
|
423
|
+
reversedSteps.push({ method, args: call.arguments });
|
|
424
|
+
node = callee.object;
|
|
425
|
+
}
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
function rewriteSelect(path, pbName, table, steps, flags, state) {
|
|
429
|
+
if (consumesDataErrorEnvelope(path)) {
|
|
430
|
+
flagEnvelopeManualReview(path, "from.select", flags);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const select = steps[0];
|
|
434
|
+
const tail = steps.slice(1);
|
|
435
|
+
// Walk the chain after .select() and accumulate option fields.
|
|
436
|
+
// Anything we don't recognise lands the entire chain in
|
|
437
|
+
// manual_review without mutation. The tradeoff: cleaner rewrites
|
|
438
|
+
// for the common cases, no risky guess for the edge cases.
|
|
439
|
+
const opts = {};
|
|
440
|
+
let needsReview = false;
|
|
441
|
+
const reviewNotes = [];
|
|
442
|
+
let usePagination = false; // true when .range() / .limit() forces getList
|
|
443
|
+
let paginationPage = 1;
|
|
444
|
+
let paginationPerPage = 0;
|
|
445
|
+
// .select() fields (PB calls this `fields`)
|
|
446
|
+
const fieldsValue = extractSelectFields(select);
|
|
447
|
+
if (fieldsValue !== null)
|
|
448
|
+
opts.fields = jscodeshift.literal(fieldsValue);
|
|
449
|
+
for (const step of tail) {
|
|
450
|
+
switch (step.method) {
|
|
451
|
+
case "eq": {
|
|
452
|
+
const result = buildEqFilter(step);
|
|
453
|
+
if (!result) {
|
|
454
|
+
needsReview = true;
|
|
455
|
+
reviewNotes.push(`.eq() arguments not in the (column-literal, value) shape — manual review.`);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
// Multiple .eq() calls concatenate via PB's `&&` combinator.
|
|
459
|
+
opts.filter = result.identifierValue
|
|
460
|
+
? combineFilterDynamic(opts.filter, result.expression)
|
|
461
|
+
: combineFilterStatic(opts.filter, result.expression);
|
|
462
|
+
if (result.identifierValue) {
|
|
463
|
+
// Escaping a runtime string into a PB filter is the user's
|
|
464
|
+
// job — the plan calls this out explicitly in the mapping
|
|
465
|
+
// table. Annotate AND downgrade to manual_review so the
|
|
466
|
+
// file walker surfaces it; the rewrite proceeds because
|
|
467
|
+
// the template-literal shape is correct, only the
|
|
468
|
+
// escape-safety is unverifiable at codemod time.
|
|
469
|
+
needsReview = true;
|
|
470
|
+
reviewNotes.push(`.eq(${JSON.stringify(result.column)}, <identifier>) — verify the value is escape-safe for PB filter strings (no double-quotes / backslashes).`);
|
|
471
|
+
}
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
case "order": {
|
|
475
|
+
const sortFrag = buildSortFragment(step);
|
|
476
|
+
if (!sortFrag) {
|
|
477
|
+
needsReview = true;
|
|
478
|
+
reviewNotes.push(`.order() arguments not in the recognised shape — manual review.`);
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
opts.sort = jscodeshift.literal(opts.sort ? `${literalString(opts.sort)},${sortFrag}` : sortFrag);
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
case "limit": {
|
|
485
|
+
const arg = step.args[0];
|
|
486
|
+
const n = extractNumber(arg);
|
|
487
|
+
if (n === null) {
|
|
488
|
+
needsReview = true;
|
|
489
|
+
reviewNotes.push(`.limit() argument isn't a literal number — manual review.`);
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
// perPage <= 0 isn't a valid PB pagination value (it'd
|
|
493
|
+
// mean "zero rows per page" which translates to "always
|
|
494
|
+
// empty" — likely not what the user meant). Refuse to
|
|
495
|
+
// mutate; flag for review. Codex P2 round 7.
|
|
496
|
+
if (n <= 0) {
|
|
497
|
+
flags.push(makeFlag(path, "manual_review", "from.select.limit_invalid", `from(${JSON.stringify(table)}).select(...).limit(${n}) — non-positive limit doesn't translate to PB pagination. Manual review.`));
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
usePagination = true;
|
|
501
|
+
paginationPerPage = Math.max(paginationPerPage, n);
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
case "range": {
|
|
505
|
+
// Supabase .range(start, end) is INCLUSIVE on both ends. PB
|
|
506
|
+
// .getList(page, perPage) is page-based. The translation
|
|
507
|
+
// (start=0, end=N-1) → page=1, perPage=N works when start
|
|
508
|
+
// is 0; non-zero start gets best-effort + manual_review.
|
|
509
|
+
const startArg = step.args[0];
|
|
510
|
+
const endArg = step.args[1];
|
|
511
|
+
const startN = extractNumber(startArg);
|
|
512
|
+
const endN = extractNumber(endArg);
|
|
513
|
+
if (startN === null || endN === null) {
|
|
514
|
+
needsReview = true;
|
|
515
|
+
reviewNotes.push(`.range() arguments aren't literal numbers — manual review.`);
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
const perPage = endN - startN + 1;
|
|
519
|
+
// Empty / negative range: refuse mutation. Same reasoning
|
|
520
|
+
// as .limit(0). Codex P2 round 7.
|
|
521
|
+
if (perPage <= 0) {
|
|
522
|
+
flags.push(makeFlag(path, "manual_review", "from.select.range_invalid", `from(${JSON.stringify(table)}).select(...).range(${startN}, ${endN}) — end < start; PB pagination has no equivalent. Manual review.`));
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
usePagination = true;
|
|
526
|
+
paginationPerPage = Math.max(paginationPerPage, perPage);
|
|
527
|
+
if (startN === 0) {
|
|
528
|
+
paginationPage = 1;
|
|
529
|
+
}
|
|
530
|
+
else if (startN % perPage === 0) {
|
|
531
|
+
paginationPage = Math.floor(startN / perPage) + 1;
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
needsReview = true;
|
|
535
|
+
reviewNotes.push(`.range(${startN}, ${endN}) — start isn't on a page boundary; PB's page math doesn't translate cleanly. Manual review.`);
|
|
536
|
+
}
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
case "single": {
|
|
540
|
+
// .single() flips the consumer's return-type contract: the
|
|
541
|
+
// user's code reads `x.id`, but getFullList returns an
|
|
542
|
+
// array. Even with a review flag, mutating the call site
|
|
543
|
+
// would produce behaviorally-wrong code that compiles
|
|
544
|
+
// cleanly. Refuse the entire chain rewrite instead.
|
|
545
|
+
// Codex P1 round 7 (2026-05-17).
|
|
546
|
+
flags.push(makeFlag(path, "manual_review", "from.select.single", `from(${JSON.stringify(table)}).select(...).single() — PB's getFullList returns an array; getFirstListItem(filter) is the single-record equivalent but takes a filter string. Rewrite this chain by hand: pick getFirstListItem with the right filter OR [0]-index the array result.`));
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
default: {
|
|
550
|
+
// Unknown step — fall out of the chunk-2 happy path. The
|
|
551
|
+
// entire chain stays as manual_review without mutation so
|
|
552
|
+
// we don't half-translate something subtle.
|
|
553
|
+
flags.push(makeFlag(path, "manual_review", `from.select.${step.method}`, `from(${JSON.stringify(table)}).select(...).${step.method}(...) has no chunk-2 rewrite — manual review.`));
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
// Build the call. Without pagination → getFullList(opts).
|
|
559
|
+
// With pagination → getList(page, perPage, opts-minus-pagination-bits).
|
|
560
|
+
let methodName;
|
|
561
|
+
let methodArgs;
|
|
562
|
+
if (usePagination) {
|
|
563
|
+
methodName = "getList";
|
|
564
|
+
const trailingOpts = buildOptionsObject(opts);
|
|
565
|
+
methodArgs = [jscodeshift.literal(paginationPage), jscodeshift.literal(paginationPerPage)];
|
|
566
|
+
if (trailingOpts)
|
|
567
|
+
methodArgs.push(trailingOpts);
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
methodName = "getFullList";
|
|
571
|
+
const optsObj = buildOptionsObject(opts);
|
|
572
|
+
methodArgs = optsObj ? [optsObj] : [];
|
|
573
|
+
}
|
|
574
|
+
const replacement = jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.identifier(pbName), jscodeshift.identifier("collection")), [jscodeshift.literal(table)]), jscodeshift.identifier(methodName)), methodArgs);
|
|
575
|
+
const kind = needsReview ? "manual_review" : "rewritten";
|
|
576
|
+
const baseNote = `from(${JSON.stringify(table)}).select(...) → pb.collection(...).${methodName}(...)`;
|
|
577
|
+
const note = needsReview ? `${baseNote} — ${reviewNotes.join(" · ")}` : baseNote;
|
|
578
|
+
applyRewrite(path, replacement, kind, "from.select", note, flags, state);
|
|
579
|
+
}
|
|
580
|
+
/** Extract the comma-separated columns from a `.select("a,b")` call.
|
|
581
|
+
* Returns null when the args don't match the literal-string shape
|
|
582
|
+
* (caller falls back to no `fields` option). */
|
|
583
|
+
function extractSelectFields(select) {
|
|
584
|
+
if (select.args.length === 0)
|
|
585
|
+
return null;
|
|
586
|
+
const first = select.args[0];
|
|
587
|
+
if ((first.type === "Literal" || first.type === "StringLiteral") &&
|
|
588
|
+
typeof first.value === "string") {
|
|
589
|
+
const val = first.value;
|
|
590
|
+
if (val === "*" || val.length === 0)
|
|
591
|
+
return null;
|
|
592
|
+
return val;
|
|
593
|
+
}
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
function buildEqFilter(step) {
|
|
597
|
+
if (step.args.length < 2)
|
|
598
|
+
return null;
|
|
599
|
+
const colArg = step.args[0];
|
|
600
|
+
const valArg = step.args[1];
|
|
601
|
+
if (!(colArg.type === "Literal" || colArg.type === "StringLiteral") ||
|
|
602
|
+
typeof colArg.value !== "string") {
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
const column = colArg.value;
|
|
606
|
+
// Literal value → static filter string. `col = "value"` for
|
|
607
|
+
// strings, `col = 42` for numbers. jscodeshift's tsx parser
|
|
608
|
+
// emits NumericLiteral / BooleanLiteral as distinct types from
|
|
609
|
+
// the generic Literal — accept all four.
|
|
610
|
+
if (valArg.type === "Literal" ||
|
|
611
|
+
valArg.type === "StringLiteral" ||
|
|
612
|
+
valArg.type === "NumericLiteral" ||
|
|
613
|
+
valArg.type === "BooleanLiteral") {
|
|
614
|
+
const v = valArg.value;
|
|
615
|
+
if (typeof v === "string") {
|
|
616
|
+
return {
|
|
617
|
+
expression: jscodeshift.literal(`${column} = "${escapeFilterString(v)}"`),
|
|
618
|
+
column,
|
|
619
|
+
identifierValue: false,
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
if (typeof v === "number" || typeof v === "boolean") {
|
|
623
|
+
return {
|
|
624
|
+
expression: jscodeshift.literal(`${column} = ${v}`),
|
|
625
|
+
column,
|
|
626
|
+
identifierValue: false,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
// Non-literal (identifier, member access, etc.) → template literal
|
|
632
|
+
// so the value is interpolated at runtime. Escaping is the user's
|
|
633
|
+
// responsibility — flagged via reviewNotes upstream.
|
|
634
|
+
const template = jscodeshift.templateLiteral([
|
|
635
|
+
jscodeshift.templateElement({ raw: `${column} = "`, cooked: `${column} = "` }, false),
|
|
636
|
+
jscodeshift.templateElement({ raw: `"`, cooked: `"` }, true),
|
|
637
|
+
], [valArg]);
|
|
638
|
+
return {
|
|
639
|
+
expression: template,
|
|
640
|
+
column,
|
|
641
|
+
identifierValue: true,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
function escapeFilterString(s) {
|
|
645
|
+
// PB filter strings interpret BOTH `\` and `"` inside double-
|
|
646
|
+
// quoted values. Escape backslashes FIRST (so a literal
|
|
647
|
+
// backslash doesn't get re-escaped by the quote step), then
|
|
648
|
+
// double-quotes. Without the backslash step, a column value
|
|
649
|
+
// like `a\b` would emit `filter: "col = \"a\b\""` which the PB
|
|
650
|
+
// parser reads as `a` + backspace + `b`. Codex P2 round 7
|
|
651
|
+
// (2026-05-17) — the dynamic-value note already warned about
|
|
652
|
+
// this; the static path needed the same treatment.
|
|
653
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Combine two filter expressions into `(a) && (b)`. Both `a` and
|
|
657
|
+
* `b` may be string literals OR template literals.
|
|
658
|
+
*/
|
|
659
|
+
function combineFilterStatic(current, next) {
|
|
660
|
+
if (!current)
|
|
661
|
+
return next;
|
|
662
|
+
const a = literalString(current);
|
|
663
|
+
const b = literalString(next);
|
|
664
|
+
if (a !== null && b !== null) {
|
|
665
|
+
return jscodeshift.literal(`(${a}) && (${b})`);
|
|
666
|
+
}
|
|
667
|
+
// One side is dynamic — fall through to dynamic combiner.
|
|
668
|
+
return combineFilterDynamic(current, next);
|
|
669
|
+
}
|
|
670
|
+
function combineFilterDynamic(current, next) {
|
|
671
|
+
if (!current)
|
|
672
|
+
return next;
|
|
673
|
+
// Build a template literal that wraps both sides in parens with `&&`.
|
|
674
|
+
return jscodeshift.templateLiteral([
|
|
675
|
+
jscodeshift.templateElement({ raw: "(", cooked: "(" }, false),
|
|
676
|
+
jscodeshift.templateElement({ raw: ") && (", cooked: ") && (" }, false),
|
|
677
|
+
jscodeshift.templateElement({ raw: ")", cooked: ")" }, true),
|
|
678
|
+
], [current, next]);
|
|
679
|
+
}
|
|
680
|
+
function literalString(node) {
|
|
681
|
+
if (node?.type === "Literal" || node?.type === "StringLiteral") {
|
|
682
|
+
const v = node.value;
|
|
683
|
+
if (typeof v === "string")
|
|
684
|
+
return v;
|
|
685
|
+
}
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
function buildSortFragment(step) {
|
|
689
|
+
if (step.args.length < 1)
|
|
690
|
+
return null;
|
|
691
|
+
const colArg = step.args[0];
|
|
692
|
+
if (!(colArg.type === "Literal" || colArg.type === "StringLiteral") ||
|
|
693
|
+
typeof colArg.value !== "string") {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
const column = colArg.value;
|
|
697
|
+
let ascending = true;
|
|
698
|
+
if (step.args.length >= 2) {
|
|
699
|
+
const opts = step.args[1];
|
|
700
|
+
if (opts.type === "ObjectExpression") {
|
|
701
|
+
const ascProp = findObjectProp(opts, "ascending");
|
|
702
|
+
if (ascProp?.type === "Literal" || ascProp?.type === "BooleanLiteral") {
|
|
703
|
+
ascending = ascProp.value === true;
|
|
704
|
+
}
|
|
705
|
+
else if (ascProp) {
|
|
706
|
+
// Non-literal ascending — can't statically decide. Return null
|
|
707
|
+
// so the caller routes to manual_review.
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return ascending ? column : `-${column}`;
|
|
713
|
+
}
|
|
714
|
+
function extractNumber(node) {
|
|
715
|
+
if (!node)
|
|
716
|
+
return null;
|
|
717
|
+
if (node.type === "Literal" || node.type === "NumericLiteral") {
|
|
718
|
+
const v = node.value;
|
|
719
|
+
if (typeof v === "number")
|
|
720
|
+
return v;
|
|
721
|
+
}
|
|
722
|
+
if (node.type === "UnaryExpression") {
|
|
723
|
+
const u = node;
|
|
724
|
+
if (u.operator === "-") {
|
|
725
|
+
const inner = extractNumber(u.argument);
|
|
726
|
+
if (inner !== null)
|
|
727
|
+
return -inner;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
function buildOptionsObject(opts) {
|
|
733
|
+
const props = [];
|
|
734
|
+
if (opts.fields !== undefined) {
|
|
735
|
+
props.push(jscodeshift.property("init", jscodeshift.identifier("fields"), opts.fields));
|
|
736
|
+
}
|
|
737
|
+
if (opts.filter !== undefined) {
|
|
738
|
+
props.push(jscodeshift.property("init", jscodeshift.identifier("filter"), opts.filter));
|
|
739
|
+
}
|
|
740
|
+
if (opts.sort !== undefined) {
|
|
741
|
+
props.push(jscodeshift.property("init", jscodeshift.identifier("sort"), opts.sort));
|
|
742
|
+
}
|
|
743
|
+
if (props.length === 0)
|
|
744
|
+
return null;
|
|
745
|
+
return jscodeshift.objectExpression(props);
|
|
746
|
+
}
|
|
747
|
+
function rewriteInsert(path, pbName, table, insert, flags, state) {
|
|
748
|
+
if (consumesDataErrorEnvelope(path)) {
|
|
749
|
+
flagEnvelopeManualReview(path, "from.insert", flags);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
// pb.collection(T).create(arg). Supabase .insert accepts an
|
|
753
|
+
// object OR an array; PB's .create takes a single object. If the
|
|
754
|
+
// arg is an array we flag for manual review (the user wants a
|
|
755
|
+
// loop or batch helper).
|
|
756
|
+
if (insert.args.length === 0) {
|
|
757
|
+
flags.push(makeFlag(path, "manual_review", "from.insert", "from(...).insert() called with no args — manual review."));
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
const arg = insert.args[0];
|
|
761
|
+
if (arg.type === "ArrayExpression") {
|
|
762
|
+
flags.push(makeFlag(path, "manual_review", "from.insert.array", `from(${JSON.stringify(table)}).insert([...]) — PB's create takes one record at a time; wrap in a loop.`));
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
const replacement = jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.identifier(pbName), jscodeshift.identifier("collection")), [jscodeshift.literal(table)]), jscodeshift.identifier("create")), [arg]);
|
|
766
|
+
applyRewrite(path, replacement, "rewritten", "from.insert", `from(${JSON.stringify(table)}).insert({...}) → pb.collection(...).create({...})`, flags, state);
|
|
767
|
+
}
|
|
768
|
+
function rewriteUpdate(path, pbName, table, steps, flags, state) {
|
|
769
|
+
if (consumesDataErrorEnvelope(path)) {
|
|
770
|
+
flagEnvelopeManualReview(path, "from.update", flags);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
// .update({...}).eq("id", value) → pb.collection(T).update(value, {...})
|
|
774
|
+
// Anything else (.eq on a non-id column, multiple .eq, missing
|
|
775
|
+
// .eq) lands in manual_review.
|
|
776
|
+
const update = steps[0];
|
|
777
|
+
const eq = steps[1];
|
|
778
|
+
if (steps.length !== 2 || !eq || eq.method !== "eq") {
|
|
779
|
+
flags.push(makeFlag(path, "manual_review", "from.update.shape", `from(${JSON.stringify(table)}).update(...) chain isn't the .update({...}).eq("id", v) shape — manual review.`));
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
if (eq.args.length < 2) {
|
|
783
|
+
flags.push(makeFlag(path, "manual_review", "from.update.eq", "update().eq(...) call missing args — manual review."));
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
const eqCol = eq.args[0];
|
|
787
|
+
const eqVal = eq.args[1];
|
|
788
|
+
if (!((eqCol.type === "Literal" || eqCol.type === "StringLiteral") &&
|
|
789
|
+
eqCol.value === "id")) {
|
|
790
|
+
flags.push(makeFlag(path, "manual_review", "from.update.eq_non_id", `from(${JSON.stringify(table)}).update().eq() filters on a non-id column — PB's update needs an id; chunk 2 will handle filter-based updates.`));
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
if (update.args.length === 0) {
|
|
794
|
+
flags.push(makeFlag(path, "manual_review", "from.update", "update() called with no args — manual review."));
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
const replacement = jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.identifier(pbName), jscodeshift.identifier("collection")), [jscodeshift.literal(table)]), jscodeshift.identifier("update")), [eqVal, update.args[0]]);
|
|
798
|
+
applyRewrite(path, replacement, "rewritten", "from.update", `from(${JSON.stringify(table)}).update({...}).eq("id", v) → pb.collection(...).update(v, {...})`, flags, state);
|
|
799
|
+
}
|
|
800
|
+
function rewriteDelete(path, pbName, table, steps, flags, state) {
|
|
801
|
+
if (consumesDataErrorEnvelope(path)) {
|
|
802
|
+
flagEnvelopeManualReview(path, "from.delete", flags);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
// .delete().eq("id", value) → pb.collection(T).delete(value)
|
|
806
|
+
const eq = steps[1];
|
|
807
|
+
if (steps.length !== 2 || !eq || eq.method !== "eq") {
|
|
808
|
+
flags.push(makeFlag(path, "manual_review", "from.delete.shape", `from(${JSON.stringify(table)}).delete() chain isn't the .delete().eq("id", v) shape — manual review.`));
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
if (eq.args.length < 2) {
|
|
812
|
+
flags.push(makeFlag(path, "manual_review", "from.delete.eq", "delete().eq(...) call missing args — manual review."));
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
const eqCol = eq.args[0];
|
|
816
|
+
const eqVal = eq.args[1];
|
|
817
|
+
if (!((eqCol.type === "Literal" || eqCol.type === "StringLiteral") &&
|
|
818
|
+
eqCol.value === "id")) {
|
|
819
|
+
flags.push(makeFlag(path, "manual_review", "from.delete.eq_non_id", `from(${JSON.stringify(table)}).delete().eq() filters on a non-id column — PB's delete needs an id; chunk 2 will handle filter-based deletes.`));
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const replacement = jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.identifier(pbName), jscodeshift.identifier("collection")), [jscodeshift.literal(table)]), jscodeshift.identifier("delete")), [eqVal]);
|
|
823
|
+
applyRewrite(path, replacement, "rewritten", "from.delete", `from(${JSON.stringify(table)}).delete().eq("id", v) → pb.collection(...).delete(v)`, flags, state);
|
|
824
|
+
}
|
|
825
|
+
// ── Chunk 3.1: flag-only patterns (no mutation) ────────────────
|
|
826
|
+
//
|
|
827
|
+
// These Supabase APIs have no clean PocketBase equivalent: the
|
|
828
|
+
// rewriter records a flag with line/column so the file walker
|
|
829
|
+
// can surface them in the migration report, but does NOT touch
|
|
830
|
+
// the source. The user reads the report and rewrites by hand.
|
|
831
|
+
//
|
|
832
|
+
// Patterns covered:
|
|
833
|
+
// - supabase.storage.* (file upload/download model differs)
|
|
834
|
+
// - supabase.channel(...).on(...).subscribe (realtime API shape differs)
|
|
835
|
+
// - supabase.rpc("fn", args) (Postgres functions — no PB equivalent)
|
|
836
|
+
// - supabase.functions.invoke(...) (Edge Functions — host elsewhere)
|
|
837
|
+
// - supabase.from(...).upsert(...) (PB has no upsert; flag for split)
|
|
838
|
+
function flagUnsupportedPatterns(root, supabaseNames, flags) {
|
|
839
|
+
// Walk every CallExpression once, identify the pattern by
|
|
840
|
+
// looking at the callee chain. Filter by Supabase identifier
|
|
841
|
+
// root so we don't false-positive on unrelated `.storage` /
|
|
842
|
+
// `.rpc` / `.channel` calls in user code.
|
|
843
|
+
root.find(jscodeshift.CallExpression).forEach((path) => {
|
|
844
|
+
flagOneUnsupported(path, supabaseNames, flags);
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
function flagOneUnsupported(path, supabaseNames, flags) {
|
|
848
|
+
const root = findChainRoot(path.node);
|
|
849
|
+
if (!root)
|
|
850
|
+
return;
|
|
851
|
+
if (root.type !== "Identifier")
|
|
852
|
+
return;
|
|
853
|
+
if (!supabaseNames.has(root.name))
|
|
854
|
+
return;
|
|
855
|
+
// We only want to flag ONCE per chain — at the outermost call.
|
|
856
|
+
// If our parent is a MemberExpression whose .object is this
|
|
857
|
+
// node, we're not the outermost.
|
|
858
|
+
const parent = path.parent?.node;
|
|
859
|
+
if (parent?.type === "MemberExpression") {
|
|
860
|
+
const m = parent;
|
|
861
|
+
if (m.object === path.node)
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
// Decompose the chain into ordered (method, call) pairs from
|
|
865
|
+
// ROOT (closest to `supabase`) to LEAF (outermost call). We use
|
|
866
|
+
// each call's OWN arguments for the report — not the outermost
|
|
867
|
+
// wrapper's. Codex P2 round 8 (2026-05-17): the previous version
|
|
868
|
+
// pulled args from `path.node` (the outermost wrapper), so
|
|
869
|
+
// `await supabase.rpc("compute_score", args).select("x")`
|
|
870
|
+
// misreported the function name as "x", and trailing
|
|
871
|
+
// `.then()` / `.catch()` made `storage.upload` report as
|
|
872
|
+
// `storage.then` etc.
|
|
873
|
+
const steps = readChainSteps(path.node);
|
|
874
|
+
if (steps.length === 0)
|
|
875
|
+
return;
|
|
876
|
+
const first = steps[0];
|
|
877
|
+
if (first.method === "storage") {
|
|
878
|
+
// supabase.storage.from("b").upload(...).<then|catch|...>
|
|
879
|
+
// `findStorageOperation` returns the canonical operation OR
|
|
880
|
+
// a sensible non-continuation fallback. Only when the chain
|
|
881
|
+
// is genuinely `supabase.storage.from(bucket)` with nothing
|
|
882
|
+
// after does the report name `from` — and that's accurate.
|
|
883
|
+
const opStep = findStorageOperation(steps);
|
|
884
|
+
const opName = opStep?.method ?? "from";
|
|
885
|
+
flags.push(makeFlag(path, "unsupported", `storage.${opName}`, `supabase.storage.<bucket>.${opName}(...) — PocketBase models files as record-attached fields, not standalone buckets. Rewrite by hand: create a collection with a \`file\` field, then use pb.collection(c).create({ file: blob }) for uploads and pb.files.getURL(record, record.file) for URLs.`));
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
if (first.method === "channel") {
|
|
889
|
+
// supabase.channel(...).on(...).subscribe() — the `.subscribe`
|
|
890
|
+
// call is the actionable terminal even if the user chains
|
|
891
|
+
// `.then(...)` after it.
|
|
892
|
+
flags.push(makeFlag(path, "unsupported", "channel.subscribe", `supabase.channel(...).on(...).subscribe(...) — PB has realtime via pb.collection(c).subscribe("*", cb) but the event shape and filter model differ from Supabase channels. Rewrite by hand.`));
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
if (first.method === "rpc") {
|
|
896
|
+
// supabase.rpc("fn_name", args). The function name + args are
|
|
897
|
+
// on the `rpc` step itself — NOT on a downstream `.select()`
|
|
898
|
+
// or `.then()`. Use that step's call.
|
|
899
|
+
const fnName = first.call ? (readFirstStringArg(first.call) ?? "<unknown>") : "<unknown>";
|
|
900
|
+
flags.push(makeFlag(path, "unsupported", "rpc", `supabase.rpc(${JSON.stringify(fnName)}, ...) — Postgres function calls don't have a PocketBase equivalent. Move the logic into your app server code OR a PB hook (.pb_hooks/).`));
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
if (first.method === "functions" && steps[1]?.method === "invoke") {
|
|
904
|
+
const invokeStep = steps[1];
|
|
905
|
+
const fnName = invokeStep.call
|
|
906
|
+
? (readFirstStringArg(invokeStep.call) ?? "<unknown>")
|
|
907
|
+
: "<unknown>";
|
|
908
|
+
flags.push(makeFlag(path, "unsupported", "functions.invoke", `supabase.functions.invoke(${JSON.stringify(fnName)}, ...) — Edge Functions don't run on Percher. Move the function code into the Percher app server, or host it on a separate platform (Deno Deploy, Cloudflare Workers) and call it via fetch.`));
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
// `from.upsert` is handled inside rewriteFromChain (the
|
|
912
|
+
// from-chain rewriter already walks .from() chains and is the
|
|
913
|
+
// canonical place for terminal-method classification). Flagging
|
|
914
|
+
// it here too would double up the report.
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Storage operations Percher knows the migration intent for. The
|
|
918
|
+
* walker picks the first matching step in the chain so a trailing
|
|
919
|
+
* `.then()` / `.catch()` doesn't end up as the reported pattern.
|
|
920
|
+
*
|
|
921
|
+
* Not exhaustive — keeping the set narrow has the upside of clean
|
|
922
|
+
* names in flags, but the downside that any newly-added Supabase
|
|
923
|
+
* storage method we forget to add here would fall through. The
|
|
924
|
+
* fallback `findFallbackStorageMethod` covers that case by walking
|
|
925
|
+
* the chain end-to-start and picking the last non-continuation,
|
|
926
|
+
* non-structural method, so a forgotten entry still produces a
|
|
927
|
+
* useful flag (just without the canonical name).
|
|
928
|
+
*
|
|
929
|
+
* Codex P2 round 9 (2026-05-17): added `createSignedUrls` (plural)
|
|
930
|
+
* which was missed — it's a legit storage API.
|
|
931
|
+
*/
|
|
932
|
+
const STORAGE_OPERATIONS = new Set([
|
|
933
|
+
"upload",
|
|
934
|
+
"uploadToSignedUrl",
|
|
935
|
+
"download",
|
|
936
|
+
"list",
|
|
937
|
+
"remove",
|
|
938
|
+
"move",
|
|
939
|
+
"copy",
|
|
940
|
+
"createSignedUrl",
|
|
941
|
+
"createSignedUrls",
|
|
942
|
+
"createSignedUploadUrl",
|
|
943
|
+
"getPublicUrl",
|
|
944
|
+
"update",
|
|
945
|
+
"exists",
|
|
946
|
+
"info",
|
|
947
|
+
]);
|
|
948
|
+
/**
|
|
949
|
+
* Promise / standard-JS continuations the walker skips when
|
|
950
|
+
* looking for the "actual" storage operation in a chain. A
|
|
951
|
+
* trailing `.then(...)` or `.catch(...)` after the storage call
|
|
952
|
+
* shouldn't be reported as the operation.
|
|
953
|
+
*/
|
|
954
|
+
const CHAIN_CONTINUATIONS = new Set(["then", "catch", "finally"]);
|
|
955
|
+
function findStorageOperation(steps) {
|
|
956
|
+
// First pass: known storage operations. Stable canonical names
|
|
957
|
+
// are preferred when the user uses a known method.
|
|
958
|
+
for (const step of steps) {
|
|
959
|
+
if (STORAGE_OPERATIONS.has(step.method))
|
|
960
|
+
return step;
|
|
961
|
+
}
|
|
962
|
+
// Fallback: any method that's not a Promise continuation and
|
|
963
|
+
// not the structural `.from(bucket)` / `.storage`. Walk
|
|
964
|
+
// END-TO-START so trailing `.then()` doesn't shadow the real
|
|
965
|
+
// operation. Codex P2 round 9 fix: previously fell back to
|
|
966
|
+
// `steps[1]` which is always `.from`, misreporting valid
|
|
967
|
+
// storage methods we forgot to add to the allowlist as
|
|
968
|
+
// `storage.from`.
|
|
969
|
+
for (let i = steps.length - 1; i >= 0; i--) {
|
|
970
|
+
const step = steps[i];
|
|
971
|
+
if (step.method === "storage" || step.method === "from")
|
|
972
|
+
continue;
|
|
973
|
+
if (CHAIN_CONTINUATIONS.has(step.method))
|
|
974
|
+
continue;
|
|
975
|
+
return step;
|
|
976
|
+
}
|
|
977
|
+
return null;
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Walk down callee.object until we hit a non-MemberExpression /
|
|
981
|
+
* non-CallExpression node. Returns the root expression (typically
|
|
982
|
+
* an Identifier like `supabase` or `sb`).
|
|
983
|
+
*/
|
|
984
|
+
function findChainRoot(node) {
|
|
985
|
+
let cursor = node;
|
|
986
|
+
while (true) {
|
|
987
|
+
if (cursor.type === "CallExpression") {
|
|
988
|
+
const c = cursor;
|
|
989
|
+
if (c.callee.type !== "MemberExpression")
|
|
990
|
+
return null;
|
|
991
|
+
cursor = c.callee.object;
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
if (cursor.type === "MemberExpression") {
|
|
995
|
+
cursor = cursor.object;
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
return cursor;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
function readChainSteps(outer) {
|
|
1002
|
+
const reversed = [];
|
|
1003
|
+
let cursor = outer;
|
|
1004
|
+
while (true) {
|
|
1005
|
+
if (cursor.type === "CallExpression") {
|
|
1006
|
+
const call = cursor;
|
|
1007
|
+
if (call.callee.type !== "MemberExpression")
|
|
1008
|
+
break;
|
|
1009
|
+
const callee = call.callee;
|
|
1010
|
+
if (callee.property.type === "Identifier") {
|
|
1011
|
+
reversed.push({
|
|
1012
|
+
method: callee.property.name,
|
|
1013
|
+
call,
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
cursor = callee.object;
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
if (cursor.type === "MemberExpression") {
|
|
1020
|
+
const m = cursor;
|
|
1021
|
+
if (m.property.type === "Identifier") {
|
|
1022
|
+
reversed.push({
|
|
1023
|
+
method: m.property.name,
|
|
1024
|
+
call: null,
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
cursor = m.object;
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
break;
|
|
1031
|
+
}
|
|
1032
|
+
return reversed.reverse();
|
|
1033
|
+
}
|
|
1034
|
+
function readFirstStringArg(call) {
|
|
1035
|
+
const a = call.arguments[0];
|
|
1036
|
+
if (!a)
|
|
1037
|
+
return null;
|
|
1038
|
+
if (a.type === "Literal" || a.type === "StringLiteral") {
|
|
1039
|
+
const v = a.value;
|
|
1040
|
+
if (typeof v === "string")
|
|
1041
|
+
return v;
|
|
1042
|
+
}
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
function findObjectProp(obj, name) {
|
|
1046
|
+
for (const prop of obj.properties) {
|
|
1047
|
+
if ((prop.type === "Property" || prop.type === "ObjectProperty") &&
|
|
1048
|
+
"key" in prop &&
|
|
1049
|
+
prop.key.type === "Identifier" &&
|
|
1050
|
+
prop.key.name === name) {
|
|
1051
|
+
// Shorthand `{ email }`: value === key Identifier with the
|
|
1052
|
+
// same name. Synthesize a fresh Identifier so the rewriter
|
|
1053
|
+
// can wire it into the new call.
|
|
1054
|
+
if (prop.shorthand === true) {
|
|
1055
|
+
return jscodeshift.identifier(name);
|
|
1056
|
+
}
|
|
1057
|
+
if ("value" in prop) {
|
|
1058
|
+
const val = prop.value;
|
|
1059
|
+
if (val.type !== "SpreadElement" && val.type !== "RestElement") {
|
|
1060
|
+
return val;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
function makeFlag(pathOrLoc, kind, pattern, note) {
|
|
1068
|
+
// Accept either an ASTPath (which we pre-read OR read before
|
|
1069
|
+
// replace) or a pre-captured loc object. Tests pin that flags
|
|
1070
|
+
// carry line/column even after the source node was swapped out
|
|
1071
|
+
// by path.replace — capture loc EAGERLY at each call site.
|
|
1072
|
+
let line;
|
|
1073
|
+
let column;
|
|
1074
|
+
if ("node" in pathOrLoc) {
|
|
1075
|
+
const loc = pathOrLoc.node.loc;
|
|
1076
|
+
line = loc?.start.line;
|
|
1077
|
+
column = loc?.start.column;
|
|
1078
|
+
}
|
|
1079
|
+
else {
|
|
1080
|
+
line = pathOrLoc.line;
|
|
1081
|
+
column = pathOrLoc.column;
|
|
1082
|
+
}
|
|
1083
|
+
return { kind, pattern, line, column, note };
|
|
1084
|
+
}
|
|
1085
|
+
/** Capture line/column from a path BEFORE the replace clobbers it. */
|
|
1086
|
+
function captureLoc(path) {
|
|
1087
|
+
const loc = path.node.loc;
|
|
1088
|
+
return { line: loc?.start.line, column: loc?.start.column };
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Single entry point for every AST mutation: captures loc, swaps
|
|
1092
|
+
* the node, pushes a flag, and flips state.mutated. Centralising
|
|
1093
|
+
* these three steps eliminates the easy-to-miss bug where one of
|
|
1094
|
+
* them is forgotten (which surfaced as the round-3 line/col-loss
|
|
1095
|
+
* bug AND the round-4 envelope mutated-detection bug).
|
|
1096
|
+
*/
|
|
1097
|
+
function applyRewrite(path, replacement, kind, pattern, note, flags, state) {
|
|
1098
|
+
const loc = captureLoc(path);
|
|
1099
|
+
path.replace(replacement);
|
|
1100
|
+
flags.push(makeFlag(loc, kind, pattern, note));
|
|
1101
|
+
state.mutated = true;
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Entry point matching jscodeshift's `transform` signature, in case
|
|
1105
|
+
* a caller wants to run this as a standalone codemod via the
|
|
1106
|
+
* jscodeshift CLI (`jscodeshift -t this-file.js src/`). Not used by
|
|
1107
|
+
* Percher's own file walker — that calls `rewriteSupabaseSdk`
|
|
1108
|
+
* directly for control over the flag-stream — but exposing it keeps
|
|
1109
|
+
* the door open for ad-hoc usage.
|
|
1110
|
+
*/
|
|
1111
|
+
export default function transform(file, _api, options = {}) {
|
|
1112
|
+
const result = rewriteSupabaseSdk({
|
|
1113
|
+
source: file.source,
|
|
1114
|
+
filePath: file.path,
|
|
1115
|
+
pbIdentifier: typeof options.pbIdentifier === "string" ? options.pbIdentifier : undefined,
|
|
1116
|
+
});
|
|
1117
|
+
return result.changed ? result.rewritten : null;
|
|
1118
|
+
}
|
|
1119
|
+
//# sourceMappingURL=migrate-supabase-sdk.js.map
|