@yasainet/eslint 0.0.57 → 0.0.58
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
|
@@ -1,50 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Enforce
|
|
3
|
-
* `*_COLUMNS` constant declarations.
|
|
2
|
+
* Enforce `<string literal> as const` for `*_COLUMNS` constant declarations.
|
|
4
3
|
*
|
|
5
|
-
* Apply to `**\/queries/*.query.ts
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* TypeScript
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* Apply to `**\/queries/*.query.ts` and `**\/constants/*.constant.ts`.
|
|
5
|
+
*
|
|
6
|
+
* `*_COLUMNS` 定数は Supabase の `.select()` に直接渡される。`as const` を
|
|
7
|
+
* 外すと TypeScript が `string` に widen し、Supabase の `.select<Query>()`
|
|
8
|
+
* が literal を parse できなくなって row 型推論が壊れる(戻り値が
|
|
9
|
+
* `GenericStringError` になる)。
|
|
10
|
+
*
|
|
11
|
+
* Allowed:
|
|
12
|
+
* const POST_DETAIL_COLUMNS = "id,url,platform" as const;
|
|
13
|
+
*
|
|
14
|
+
* Banned:
|
|
15
|
+
* const POST_DETAIL_COLUMNS = "id,url,platform"; // string に widen
|
|
16
|
+
* const POST_DETAIL_COLUMNS = ["id", "url"] as const; // 配列
|
|
17
|
+
* const POST_DETAIL_COLUMNS = [...] as const satisfies ...; // 配列 + satisfies
|
|
18
|
+
* const POST_DETAIL_COLUMNS = `id,${col}`; // template literal
|
|
19
|
+
*
|
|
20
|
+
* Why: シンプルな string literal を `as const` で保つだけで、Supabase の
|
|
21
|
+
* 型推論(row 型 / column 名タイポ検出)はすべて自動で効く。runtime helper
|
|
22
|
+
* (`joinColumns` 等)は不要。
|
|
11
23
|
*/
|
|
12
24
|
|
|
13
25
|
const COLUMNS_NAME = /^[A-Z][A-Z0-9_]*_COLUMNS$/;
|
|
14
26
|
|
|
15
|
-
|
|
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) {
|
|
27
|
+
function isStringAsConst(initNode) {
|
|
29
28
|
if (!initNode) return false;
|
|
30
|
-
if (initNode.type !== "
|
|
31
|
-
const
|
|
32
|
-
if (
|
|
33
|
-
const ann = inner.typeAnnotation;
|
|
34
|
-
if (!ann) return false;
|
|
35
|
-
if (ann.type !== "TSTypeReference") return false;
|
|
29
|
+
if (initNode.type !== "TSAsExpression") return false;
|
|
30
|
+
const ann = initNode.typeAnnotation;
|
|
31
|
+
if (!ann || ann.type !== "TSTypeReference") return false;
|
|
36
32
|
if (ann.typeName.type !== "Identifier") return false;
|
|
37
33
|
if (ann.typeName.name !== "const") return false;
|
|
38
|
-
|
|
39
|
-
return
|
|
34
|
+
const inner = initNode.expression;
|
|
35
|
+
if (inner.type !== "Literal") return false;
|
|
36
|
+
return typeof inner.value === "string";
|
|
40
37
|
}
|
|
41
38
|
|
|
42
39
|
export const supabaseColumnsSatisfiesRule = {
|
|
43
40
|
meta: {
|
|
44
41
|
type: "problem",
|
|
45
42
|
messages: {
|
|
46
|
-
|
|
47
|
-
'Column constant `{{ name }}` must
|
|
43
|
+
shape:
|
|
44
|
+
'Column constant `{{ name }}` must be `"<comma-separated columns>" as const`. `as const` を外すと Supabase の `.select()` 型推論が壊れる。配列 / template literal も不可。',
|
|
48
45
|
},
|
|
49
46
|
schema: [],
|
|
50
47
|
},
|
|
@@ -53,15 +50,11 @@ export const supabaseColumnsSatisfiesRule = {
|
|
|
53
50
|
VariableDeclarator(node) {
|
|
54
51
|
if (node.id.type !== "Identifier") return;
|
|
55
52
|
if (!COLUMNS_NAME.test(node.id.name)) return;
|
|
56
|
-
|
|
57
|
-
|
|
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;
|
|
53
|
+
if (!node.init) return;
|
|
54
|
+
if (isStringAsConst(node.init)) return;
|
|
62
55
|
context.report({
|
|
63
56
|
node: node.id,
|
|
64
|
-
messageId: "
|
|
57
|
+
messageId: "shape",
|
|
65
58
|
data: { name: node.id.name },
|
|
66
59
|
});
|
|
67
60
|
},
|
|
@@ -1,59 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Enforce
|
|
2
|
+
* Enforce explicit column lists for Supabase `.select()` calls.
|
|
3
3
|
*
|
|
4
|
-
* Apply to `**\/queries/*.query.ts`.
|
|
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.
|
|
4
|
+
* Apply to `**\/queries/*.query.ts`. `.select()` の引数は次のいずれかでなければならない:
|
|
11
5
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
6
|
+
* - inline string literal(例: `.select("id,url,platform")`)
|
|
7
|
+
* - `*_COLUMNS` という UPPER_SNAKE 命名の identifier(例: `.select(POST_DETAIL_COLUMNS)`)
|
|
8
|
+
*
|
|
9
|
+
* `*_COLUMNS` 定数は companion rule `supabase-columns-satisfies` で
|
|
10
|
+
* `<string literal> as const` の形が強制される。これにより:
|
|
11
|
+
*
|
|
12
|
+
* - Supabase の `.select()` は literal string を parse して row 型を推論できる
|
|
13
|
+
* - 存在しない column 名は Supabase の型推論が `SelectQueryError` として弾く(compile time)
|
|
14
|
+
* - runtime helper(`joinColumns`)は不要
|
|
16
15
|
*
|
|
17
16
|
* Banned:
|
|
18
|
-
* .select()
|
|
19
|
-
* .select("*")
|
|
20
|
-
* .select(
|
|
21
|
-
* .select(
|
|
22
|
-
* .select(
|
|
23
|
-
* .select(someVar) non-conforming variable
|
|
17
|
+
* .select() implicit "all columns"
|
|
18
|
+
* .select("*") silent exposure of new schema columns
|
|
19
|
+
* .select(`${x},y`) dynamic concatenation
|
|
20
|
+
* .select(cols.join(",")) runtime expression
|
|
21
|
+
* .select(someVar) non-conforming variable
|
|
24
22
|
*
|
|
25
23
|
* Allowed:
|
|
26
|
-
* .select(
|
|
24
|
+
* .select("id,url,platform") inline literal
|
|
25
|
+
* .select(POST_DETAIL_COLUMNS) *_COLUMNS named constant
|
|
27
26
|
*
|
|
28
|
-
* Why: column lists must be (1)
|
|
29
|
-
*
|
|
30
|
-
* For column-level access control, use Postgres views
|
|
27
|
+
* Why: column lists must be (1) statically analyzable for grep / review,
|
|
28
|
+
* (2) literal so Supabase can infer the row shape, (3) never silently grow
|
|
29
|
+
* on schema additions. For column-level access control, use Postgres views
|
|
30
|
+
* (`from("posts_public")`).
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
const COLUMNS_NAME = /^[A-Z][A-Z0-9_]*_COLUMNS$/;
|
|
34
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
35
|
export const supabaseSelectTypedColumnsRule = {
|
|
46
36
|
meta: {
|
|
47
37
|
type: "problem",
|
|
48
38
|
messages: {
|
|
49
39
|
noArgs:
|
|
50
|
-
"Empty `.select()` returns all columns implicitly. Pass
|
|
51
|
-
|
|
52
|
-
'
|
|
40
|
+
"Empty `.select()` returns all columns implicitly. Pass a string literal or a `*_COLUMNS` constant.",
|
|
41
|
+
wildcard:
|
|
42
|
+
'`.select("*")` exposes new schema columns silently. Enumerate columns explicitly.',
|
|
43
|
+
template:
|
|
44
|
+
"Template literal in `.select()` defeats type inference. Use a string literal or a `*_COLUMNS` constant.",
|
|
53
45
|
shapeArg:
|
|
54
|
-
|
|
46
|
+
"`.select()` argument must be a string literal or a `*_COLUMNS` identifier.",
|
|
55
47
|
naming:
|
|
56
|
-
"Column constant `{{ name }}` must be UPPER_SNAKE_CASE ending with `_COLUMNS` (e.g.
|
|
48
|
+
"Column constant `{{ name }}` must be UPPER_SNAKE_CASE ending with `_COLUMNS` (e.g. POST_DETAIL_COLUMNS).",
|
|
57
49
|
},
|
|
58
50
|
schema: [],
|
|
59
51
|
},
|
|
@@ -71,24 +63,34 @@ export const supabaseSelectTypedColumnsRule = {
|
|
|
71
63
|
|
|
72
64
|
const arg = node.arguments[0];
|
|
73
65
|
|
|
74
|
-
if (arg.type === "Literal"
|
|
75
|
-
|
|
66
|
+
if (arg.type === "Literal") {
|
|
67
|
+
if (typeof arg.value !== "string") {
|
|
68
|
+
context.report({ node: arg, messageId: "shapeArg" });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (arg.value.trim() === "*") {
|
|
72
|
+
context.report({ node: arg, messageId: "wildcard" });
|
|
73
|
+
}
|
|
76
74
|
return;
|
|
77
75
|
}
|
|
78
76
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
context.report({ node: arg, messageId: "shapeArg" });
|
|
77
|
+
if (arg.type === "TemplateLiteral") {
|
|
78
|
+
context.report({ node: arg, messageId: "template" });
|
|
82
79
|
return;
|
|
83
80
|
}
|
|
84
81
|
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
82
|
+
if (arg.type === "Identifier") {
|
|
83
|
+
if (!COLUMNS_NAME.test(arg.name)) {
|
|
84
|
+
context.report({
|
|
85
|
+
node: arg,
|
|
86
|
+
messageId: "naming",
|
|
87
|
+
data: { name: arg.name },
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
91
|
}
|
|
92
|
+
|
|
93
|
+
context.report({ node: arg, messageId: "shapeArg" });
|
|
92
94
|
},
|
|
93
95
|
};
|
|
94
96
|
},
|