@yasainet/eslint 0.0.54 → 0.0.55
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/package.json
CHANGED
|
@@ -6,6 +6,8 @@ import { noAnyReturnRule } from "./no-any-return.mjs";
|
|
|
6
6
|
import { queriesExportRule } from "./queries-export.mjs";
|
|
7
7
|
import { queriesNamespaceImportRule } from "./queries-namespace-import.mjs";
|
|
8
8
|
import { schemaNamingRule } from "./schema-naming.mjs";
|
|
9
|
+
import { supabaseColumnsSatisfiesRule } from "./supabase-columns-satisfies.mjs";
|
|
10
|
+
import { supabaseSelectTypedColumnsRule } from "./supabase-select-typed-columns.mjs";
|
|
9
11
|
|
|
10
12
|
/** Single plugin object to avoid ESLint "Cannot redefine plugin" errors. */
|
|
11
13
|
export const localPlugin = {
|
|
@@ -18,5 +20,7 @@ export const localPlugin = {
|
|
|
18
20
|
"queries-export": queriesExportRule,
|
|
19
21
|
"queries-namespace-import": queriesNamespaceImportRule,
|
|
20
22
|
"schema-naming": schemaNamingRule,
|
|
23
|
+
"supabase-columns-satisfies": supabaseColumnsSatisfiesRule,
|
|
24
|
+
"supabase-select-typed-columns": supabaseSelectTypedColumnsRule,
|
|
21
25
|
},
|
|
22
26
|
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enforce `[...] as const satisfies readonly (keyof Tables<"...">)[]` for
|
|
3
|
+
* `*_COLUMNS` constant declarations.
|
|
4
|
+
*
|
|
5
|
+
* Apply to `**\/queries/*.query.ts`. Without `as const satisfies`, typos
|
|
6
|
+
* in column names slip past the lint phase — Supabase only complains at
|
|
7
|
+
* runtime when the query executes. The `satisfies` annotation forces
|
|
8
|
+
* TypeScript to validate every entry against the schema before the code
|
|
9
|
+
* ever runs. The actual `keyof Tables<"...">` content is left to the type
|
|
10
|
+
* checker; the lint rule only verifies the syntactic shape.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const COLUMNS_NAME = /^[A-Z][A-Z0-9_]*_COLUMNS$/;
|
|
14
|
+
|
|
15
|
+
/** Unwrap nested type assertions and find the underlying expression. */
|
|
16
|
+
function unwrapTypeAssertions(node) {
|
|
17
|
+
let current = node;
|
|
18
|
+
while (
|
|
19
|
+
current &&
|
|
20
|
+
(current.type === "TSAsExpression" ||
|
|
21
|
+
current.type === "TSSatisfiesExpression")
|
|
22
|
+
) {
|
|
23
|
+
current = current.expression;
|
|
24
|
+
}
|
|
25
|
+
return current;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isAsConstSatisfies(initNode) {
|
|
29
|
+
if (!initNode) return false;
|
|
30
|
+
if (initNode.type !== "TSSatisfiesExpression") return false;
|
|
31
|
+
const inner = initNode.expression;
|
|
32
|
+
if (inner.type !== "TSAsExpression") return false;
|
|
33
|
+
const ann = inner.typeAnnotation;
|
|
34
|
+
if (!ann) return false;
|
|
35
|
+
if (ann.type !== "TSTypeReference") return false;
|
|
36
|
+
if (ann.typeName.type !== "Identifier") return false;
|
|
37
|
+
if (ann.typeName.name !== "const") return false;
|
|
38
|
+
if (inner.expression.type !== "ArrayExpression") return false;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const supabaseColumnsSatisfiesRule = {
|
|
43
|
+
meta: {
|
|
44
|
+
type: "problem",
|
|
45
|
+
messages: {
|
|
46
|
+
missing:
|
|
47
|
+
'Column constant `{{ name }}` must use `[...] as const satisfies readonly (keyof Tables<"table">)[]` so column names are validated against the schema at compile time.',
|
|
48
|
+
},
|
|
49
|
+
schema: [],
|
|
50
|
+
},
|
|
51
|
+
create(context) {
|
|
52
|
+
return {
|
|
53
|
+
VariableDeclarator(node) {
|
|
54
|
+
if (node.id.type !== "Identifier") return;
|
|
55
|
+
if (!COLUMNS_NAME.test(node.id.name)) return;
|
|
56
|
+
// Only enforce on array initializers. String literals like
|
|
57
|
+
// POST_UPSERT_CONFLICT_COLUMNS are PostgREST conflict-target specs,
|
|
58
|
+
// not column lists, and are out of scope.
|
|
59
|
+
const inner = unwrapTypeAssertions(node.init);
|
|
60
|
+
if (!inner || inner.type !== "ArrayExpression") return;
|
|
61
|
+
if (isAsConstSatisfies(node.init)) return;
|
|
62
|
+
context.report({
|
|
63
|
+
node: node.id,
|
|
64
|
+
messageId: "missing",
|
|
65
|
+
data: { name: node.id.name },
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enforce typed column constants for Supabase `.select()` calls.
|
|
3
|
+
*
|
|
4
|
+
* Apply to `**\/queries/*.query.ts`. Forces `.select()` to take the form
|
|
5
|
+
* `joinColumns(<X_COLUMNS>)` where `X_COLUMNS` is an UPPER_SNAKE identifier
|
|
6
|
+
* ending with `_COLUMNS`. The identifier is expected to be declared with
|
|
7
|
+
* `as const satisfies readonly (keyof Tables<"table">)[]` so the column
|
|
8
|
+
* names are validated against the schema at compile time. The `satisfies`
|
|
9
|
+
* shape itself is enforced by the companion `supabase-columns-satisfies`
|
|
10
|
+
* rule.
|
|
11
|
+
*
|
|
12
|
+
* `joinColumns()` is a project-supplied helper that comma-joins a const
|
|
13
|
+
* string tuple while preserving the literal string type so Supabase's
|
|
14
|
+
* `.select()` type parser can infer the row shape (a plain `.join(",")`
|
|
15
|
+
* widens to `string` and breaks inference).
|
|
16
|
+
*
|
|
17
|
+
* Banned:
|
|
18
|
+
* .select() implicit "all columns"
|
|
19
|
+
* .select("*") silent exposure of new schema columns
|
|
20
|
+
* .select("id, name") inline literal, invisible to grep
|
|
21
|
+
* .select(`${x}, y`) dynamic concatenation
|
|
22
|
+
* .select(POST_LIST_COLUMNS.join(",")) plain .join widens to `string`, breaks inference
|
|
23
|
+
* .select(someVar) non-conforming variable
|
|
24
|
+
*
|
|
25
|
+
* Allowed:
|
|
26
|
+
* .select(joinColumns(POST_LIST_COLUMNS)) typed constant via project helper
|
|
27
|
+
*
|
|
28
|
+
* Why: column lists must be (1) named for grep / review, (2) checked
|
|
29
|
+
* against the schema, (3) never silently grow on schema additions.
|
|
30
|
+
* For column-level access control, use Postgres views (`from("posts_public")`).
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const COLUMNS_NAME = /^[A-Z][A-Z0-9_]*_COLUMNS$/;
|
|
34
|
+
|
|
35
|
+
function asJoinColumnsCall(arg) {
|
|
36
|
+
if (!arg) return null;
|
|
37
|
+
if (arg.type !== "CallExpression") return null;
|
|
38
|
+
if (arg.callee.type !== "Identifier") return null;
|
|
39
|
+
if (arg.callee.name !== "joinColumns") return null;
|
|
40
|
+
if (arg.arguments.length !== 1) return null;
|
|
41
|
+
if (arg.arguments[0].type !== "Identifier") return null;
|
|
42
|
+
return arg.arguments[0];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const supabaseSelectTypedColumnsRule = {
|
|
46
|
+
meta: {
|
|
47
|
+
type: "problem",
|
|
48
|
+
messages: {
|
|
49
|
+
noArgs:
|
|
50
|
+
"Empty `.select()` returns all columns implicitly. Pass `joinColumns(<X_COLUMNS>)` where X_COLUMNS is a typed constant.",
|
|
51
|
+
literalArg:
|
|
52
|
+
'Inline `.select()` argument is forbidden. Define `const X_COLUMNS = [...] as const satisfies readonly (keyof Tables<"table">)[];` and call `.select(joinColumns(X_COLUMNS))`. Use Postgres views for column-level access control.',
|
|
53
|
+
shapeArg:
|
|
54
|
+
'`.select()` argument must be `joinColumns(<X_COLUMNS>)`. Other expressions defeat type inference and column-level review.',
|
|
55
|
+
naming:
|
|
56
|
+
"Column constant `{{ name }}` must be UPPER_SNAKE_CASE ending with `_COLUMNS` (e.g. POST_LIST_COLUMNS, POST_DETAIL_COLUMNS).",
|
|
57
|
+
},
|
|
58
|
+
schema: [],
|
|
59
|
+
},
|
|
60
|
+
create(context) {
|
|
61
|
+
return {
|
|
62
|
+
CallExpression(node) {
|
|
63
|
+
if (node.callee.type !== "MemberExpression") return;
|
|
64
|
+
if (node.callee.property.type !== "Identifier") return;
|
|
65
|
+
if (node.callee.property.name !== "select") return;
|
|
66
|
+
|
|
67
|
+
if (node.arguments.length === 0) {
|
|
68
|
+
context.report({ node, messageId: "noArgs" });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const arg = node.arguments[0];
|
|
73
|
+
|
|
74
|
+
if (arg.type === "Literal" || arg.type === "TemplateLiteral") {
|
|
75
|
+
context.report({ node: arg, messageId: "literalArg" });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const id = asJoinColumnsCall(arg);
|
|
80
|
+
if (!id) {
|
|
81
|
+
context.report({ node: arg, messageId: "shapeArg" });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!COLUMNS_NAME.test(id.name)) {
|
|
86
|
+
context.report({
|
|
87
|
+
node: id,
|
|
88
|
+
messageId: "naming",
|
|
89
|
+
data: { name: id.name },
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
};
|
package/src/common/naming.mjs
CHANGED
|
@@ -118,6 +118,25 @@ export function createNamingConfigs(featureRoot, prefixLibMapping) {
|
|
|
118
118
|
"local/queries-namespace-import": "error",
|
|
119
119
|
},
|
|
120
120
|
},
|
|
121
|
+
{
|
|
122
|
+
name: "naming/supabase-select",
|
|
123
|
+
files: featuresGlob(featureRoot, "**/queries/*.query.ts"),
|
|
124
|
+
plugins: { local: localPlugin },
|
|
125
|
+
rules: {
|
|
126
|
+
"local/supabase-select-typed-columns": "error",
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "naming/supabase-columns-satisfies",
|
|
131
|
+
files: [
|
|
132
|
+
...featuresGlob(featureRoot, "**/queries/*.query.ts"),
|
|
133
|
+
...featuresGlob(featureRoot, "**/constants/*.constant.ts"),
|
|
134
|
+
],
|
|
135
|
+
plugins: { local: localPlugin },
|
|
136
|
+
rules: {
|
|
137
|
+
"local/supabase-columns-satisfies": "error",
|
|
138
|
+
},
|
|
139
|
+
},
|
|
121
140
|
{
|
|
122
141
|
name: "naming/form-state",
|
|
123
142
|
files: featuresGlob(featureRoot, "**/*.ts"),
|