@noctcore/lint-meta-rules 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/dist/chunk-VFCX3QKZ.js +47 -0
- package/dist/prisma.cjs +442 -0
- package/dist/prisma.d.cts +174 -0
- package/dist/prisma.d.ts +174 -0
- package/dist/prisma.js +356 -0
- package/dist/resolved-config.cjs +144 -0
- package/dist/resolved-config.d.cts +46 -0
- package/dist/resolved-config.d.ts +46 -0
- package/dist/resolved-config.js +75 -0
- package/docs/rules/eslint-config-no-warn.md +71 -0
- package/docs/rules/prisma-method-surface.md +65 -0
- package/docs/rules/tenant-model-registry-parity.md +117 -0
- package/package.json +13 -2
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# `eslint-config-no-warn`
|
|
2
|
+
|
|
3
|
+
> Every rule in the RESOLVED ESLint config is `error` or `off`, never `warn`, including severities a
|
|
4
|
+
> spread preset injects.
|
|
5
|
+
|
|
6
|
+
Import it from the `resolved-config` entry point, which (unlike the main one) loads ESLint:
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { createEslintConfigNoWarnRule } from '@noctcore/lint-meta-rules/resolved-config';
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Why
|
|
13
|
+
|
|
14
|
+
A `warn` neither fails CI nor gets fixed. [`no-warn-severity`](./no-warn-severity.md) scans config
|
|
15
|
+
TEXT for a `'warn'` literal, which misses the common case: a preset spread into the config
|
|
16
|
+
(`...reactHooks.configs['recommended-latest']`) ships rules at `warn`, and no file the project owns
|
|
17
|
+
spells the word. This rule asks ESLint itself. It resolves each package's effective flat config with
|
|
18
|
+
`calculateConfigForFile` and reports every rule that ends up at severity 1, however it got there.
|
|
19
|
+
|
|
20
|
+
Use both: the text scan is instant and runs anywhere; this one is the one a preset cannot slip past.
|
|
21
|
+
|
|
22
|
+
## What it flags
|
|
23
|
+
|
|
24
|
+
For every directory matched by `packages` that holds one of `configFiles`, it resolves the config for
|
|
25
|
+
each of `probes` (glob matching only, so the probe files need not exist) and reports, once per rule
|
|
26
|
+
id, every rule whose resolved severity is `warn` (`1`, `'warn'`, or `['warn', ...]`).
|
|
27
|
+
|
|
28
|
+
It **fails closed**:
|
|
29
|
+
|
|
30
|
+
- a config that cannot be loaded (a missing import, an unbuilt workspace package) is a violation;
|
|
31
|
+
- a probe whose resolution THROWS is a violation even when another probe resolves, so a config that
|
|
32
|
+
breaks for one file shape cannot pass on the shapes that still work;
|
|
33
|
+
- a package whose config ignores every probe is a violation, since nothing was checked.
|
|
34
|
+
|
|
35
|
+
A probe that is merely ignored is fine while another probe resolves.
|
|
36
|
+
|
|
37
|
+
Async: it implements the harness's `runAsync` (`@noctcore/harness` 0.3.0 or newer).
|
|
38
|
+
|
|
39
|
+
## Factory
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
createEslintConfigNoWarnRule(options?: EslintConfigNoWarnOptions): IMetaRule
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
| Option | Type | Default | Meaning |
|
|
46
|
+
| --- | --- | --- | --- |
|
|
47
|
+
| `id` | `string` | `'eslint-config-no-warn'` | Rule id, for running more than one instance. |
|
|
48
|
+
| `packages` | `string[]` | `['.', 'apps/*', 'packages/*']` | Directories whose config is resolved: globs, or `.` for the root. A match is checked only when it holds one of `configFiles`. |
|
|
49
|
+
| `configFiles` | `string[]` | `['eslint.config.js', 'eslint.config.mjs', 'eslint.config.cjs']` | File names that mark a directory as owning a flat config. The first one present names the violation's file. |
|
|
50
|
+
| `probes` | `string[]` | `src/__lint_meta_probe__.{ts,tsx,test.ts,test.tsx}` | Files, relative to each package, the config is resolved for. Add one per file shape your config scopes blocks to (`.js`, `.vue`, `e2e/**`). |
|
|
51
|
+
| `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
|
|
52
|
+
|
|
53
|
+
## Worked example: a pnpm monorepo
|
|
54
|
+
|
|
55
|
+
Settly-style layout, where every app and package owns a config built from a shared
|
|
56
|
+
`@repo/eslint-config`, and the root config only lints tooling:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
createEslintConfigNoWarnRule({ packages: ['apps/*', 'packages/*'] });
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Deleting the three `react-hooks/*` overrides from the shared React config (so the
|
|
63
|
+
`recommended-latest` preset's `warn` shows through, with no `warn` literal anywhere) reports three
|
|
64
|
+
rules in each of the three packages that spread it.
|
|
65
|
+
|
|
66
|
+
## Notes
|
|
67
|
+
|
|
68
|
+
- The rule resolves with the `eslint` that `@noctcore/lint-meta-rules` resolves, which is the
|
|
69
|
+
consumer's own install when it is hoisted.
|
|
70
|
+
- If a config imports a workspace package that must be built first, build it before lint-meta, or
|
|
71
|
+
the rule reports that the config could not be resolved.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# `prisma-method-surface`
|
|
2
|
+
|
|
3
|
+
> The Prisma reads and writes your rules police partition the generated client's `<Model>Delegate`
|
|
4
|
+
> method surface exactly, so a Prisma upgrade cannot add an unguarded method.
|
|
5
|
+
|
|
6
|
+
Import it from the `prisma` entry point:
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { createPrismaMethodSurfaceRule } from '@noctcore/lint-meta-rules/prisma';
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Why
|
|
13
|
+
|
|
14
|
+
Rules that guard Prisma calls by method name (tenant fences, single-writer fences, transaction
|
|
15
|
+
rules) read a hand-written list of methods. The list falls behind the client: Prisma adds a method
|
|
16
|
+
(`createManyAndReturn` and `updateManyAndReturn` both arrived this way), nobody edits the list, and
|
|
17
|
+
the new method is invisible to every fence at once. It reads like ordinary code, not like evasion,
|
|
18
|
+
which is what makes the gap dangerous.
|
|
19
|
+
|
|
20
|
+
This reads the GENERATED client, which changes the moment Prisma is upgraded, and asserts the
|
|
21
|
+
configured reads and writes are exactly its delegate surface.
|
|
22
|
+
|
|
23
|
+
## What it flags
|
|
24
|
+
|
|
25
|
+
- a delegate method that is neither a configured read nor a configured write (the upgrade case);
|
|
26
|
+
- a configured method the client no longer exposes;
|
|
27
|
+
- a delegate whose method surface differs from the others (the lists are model-agnostic);
|
|
28
|
+
- a method configured as both a read and a write.
|
|
29
|
+
|
|
30
|
+
It **fails closed**: no generated client under `clientGlobs` is a violation, because a checkout that
|
|
31
|
+
never ran `prisma generate` is exactly where drift hides. Run `prisma generate` before lint-meta.
|
|
32
|
+
|
|
33
|
+
Members are read line by line: a delegate method is a generic, `name<T ...>(...)`, at the interface's
|
|
34
|
+
member indentation. `fields`, the symbol brand and `$`-prefixed members are not query methods.
|
|
35
|
+
|
|
36
|
+
## Factory
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
createPrismaMethodSurfaceRule(options?: PrismaMethodSurfaceOptions): IMetaRule
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
| Option | Type | Default | Meaning |
|
|
43
|
+
| --- | --- | --- | --- |
|
|
44
|
+
| `id` | `string` | `'prisma-method-surface'` | Rule id. |
|
|
45
|
+
| `clientGlobs` | `string[]` | `['generated/prisma/models/*.ts', 'node_modules/.prisma/client/index.d.ts']` | Generated client files declaring the delegates: the `prisma-client` generator's per-model files, or the `prisma-client-js` generator's single `index.d.ts`. Point it at your generator's `output`. |
|
|
46
|
+
| `writeMethods` | `string[]` | `PRISMA_WRITE_METHODS` from `@noctcore/eslint-plugin-prisma` | The methods your rules police as writes. |
|
|
47
|
+
| `readMethods` | `string[]` | `PRISMA_READ_METHODS` from `@noctcore/eslint-plugin-prisma` | The methods your rules know to be reads. |
|
|
48
|
+
| `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
|
|
49
|
+
|
|
50
|
+
With the defaults, the rule checks the exact lists `@noctcore/eslint-plugin-prisma`'s rules read.
|
|
51
|
+
If your own rules read their own lists, pass those.
|
|
52
|
+
|
|
53
|
+
## Worked example: Settly
|
|
54
|
+
|
|
55
|
+
Settly's schema generates into `packages/database/generated/prisma` with the `prisma-client`
|
|
56
|
+
generator:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
createPrismaMethodSurfaceRule({
|
|
60
|
+
clientGlobs: ['packages/database/generated/prisma/models/*.ts'],
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Adding a method to every generated delegate reports it as unguarded; adding it to one model only
|
|
65
|
+
reports that delegate as divergent; removing the generated folder reports that no client was found.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# `tenant-model-registry-parity`
|
|
2
|
+
|
|
3
|
+
> Every tenant-bearing Prisma model is scoped by the runtime tenant extension or exempt with a
|
|
4
|
+
> reason, and the tenant lint rules resolve with exactly that registry.
|
|
5
|
+
|
|
6
|
+
Import it from the `prisma` entry point:
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { createTenantModelRegistryParityRule } from '@noctcore/lint-meta-rules/prisma';
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Why
|
|
13
|
+
|
|
14
|
+
A multi-tenant Prisma project keeps "which models are tenant-scoped?" in two hand-maintained places:
|
|
15
|
+
the model map of its tenant-scope client extension (the runtime boundary) and the model list its
|
|
16
|
+
tenant lint rules are configured with (the static boundary). Checking those two against each other
|
|
17
|
+
catches a list edited on one side. It cannot catch the expensive case: a migration adds a
|
|
18
|
+
tenant-bearing table and touches neither list, so the table gets no runtime scope and no lint
|
|
19
|
+
coverage while both lists still agree.
|
|
20
|
+
|
|
21
|
+
So the SCHEMA is the third input. The set of tenant-bearing models is derived from it, and every one
|
|
22
|
+
must be in the runtime map or in an explicit `unscopedByDesign` exemption with a written reason. A
|
|
23
|
+
new tenant-scoped model cannot be added without the guardrails learning about it.
|
|
24
|
+
|
|
25
|
+
The static side is read from the RESOLVED ESLint config, not by parsing the config file, so it
|
|
26
|
+
asserts what the rules actually receive: moving the array, spreading a different preset, or passing
|
|
27
|
+
one rule a hand-written list is all caught.
|
|
28
|
+
|
|
29
|
+
## What it flags
|
|
30
|
+
|
|
31
|
+
1. **Schema vs registry** (via `reconcileTenantRegistry` from `@noctcore/eslint-plugin-prisma`): a
|
|
32
|
+
model carrying every `tenantFields` column that is neither scoped nor exempt; a model both scoped
|
|
33
|
+
and exempt; a scoped model or exemption no schema model maps to.
|
|
34
|
+
2. **Hand scope** (when `requireHandScope`): an exemption with no `handScopedModels` columns and no
|
|
35
|
+
`handScopePending` note; a `handScopedModels` or `handScopePending` entry for a model that is not
|
|
36
|
+
exempt.
|
|
37
|
+
3. **Registry vs resolved config** (unless `eslint: false`): for each of `modelRules`, a rule that is
|
|
38
|
+
off or not configured, a rule with no `modelsOption`, a scoped model missing from the option, or
|
|
39
|
+
an option model that is not scoped; and a `handScopedRule` whose `handScopedOption` does not equal
|
|
40
|
+
`registry.handScopedModels`.
|
|
41
|
+
|
|
42
|
+
It **fails closed**: an unreadable runtime map, a missing schema, or a config that cannot be
|
|
43
|
+
resolved is a violation, never an empty registry. Schema findings are still reported when the config
|
|
44
|
+
cannot be resolved.
|
|
45
|
+
|
|
46
|
+
Async: it implements the harness's `runAsync` (`@noctcore/harness` 0.3.0 or newer).
|
|
47
|
+
|
|
48
|
+
## Factory
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
createTenantModelRegistryParityRule(options?: TenantModelRegistryParityOptions): IMetaRule
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
| Option | Type | Default | Meaning |
|
|
55
|
+
| --- | --- | --- | --- |
|
|
56
|
+
| `id` | `string` | `'tenant-model-registry-parity'` | Rule id. |
|
|
57
|
+
| `schemaPath` | `string` | `'prisma/schema.prisma'` | The schema: a `.prisma` file, or a folder whose `.prisma` files (recursively) are read together. |
|
|
58
|
+
| `tenantFields` | `string[]` | `['tenantId']` | Columns that make a model tenant-bearing; a model must carry ALL of them. |
|
|
59
|
+
| `registry` | `TenantRegistry` | `{ scopedModels: [], unscopedByDesign: {} }` | The project's registry as injected DATA: `scopedModels`, `unscopedByDesign`, `handScopedModels`, `handScopePending` (the `TenantRegistry` type from `@noctcore/eslint-plugin-prisma`). |
|
|
60
|
+
| `scopedModelsSource` | `{ file, exportName }` | none | Read the scoped models from the runtime source instead of `registry.scopedModels`: the top-level keys of the object literal assigned to `exportName` in `file`. |
|
|
61
|
+
| `requireHandScope` | `boolean` | `true` | Require every exemption to name its hand-scope columns or record why it cannot. |
|
|
62
|
+
| `eslint` | `TenantRegistryEslintOptions \| false` | `{}` | The static side (below), or `false` to check only the registry against the schema. |
|
|
63
|
+
| `registryFile` | `string` | `scopedModelsSource.file`, else `schemaPath` | Path reported for registry-side violations. |
|
|
64
|
+
| `ciCritical` | `boolean` | `true` | Whether a violation fails CI. |
|
|
65
|
+
|
|
66
|
+
`eslint` options:
|
|
67
|
+
|
|
68
|
+
| Option | Type | Default | Meaning |
|
|
69
|
+
| --- | --- | --- | --- |
|
|
70
|
+
| `cwd` | `string` | `'.'` | Directory whose ESLint config is resolved. |
|
|
71
|
+
| `probe` | `string` | `'src/__lint_meta_probe__.ts'` | File, relative to `cwd`, the config is resolved for. Match the glob your tenant rules are scoped to. |
|
|
72
|
+
| `modelRules` | `string[]` | the `noctcore-prisma` rules `no-cross-tenant-id-in-where`, `tenant-scoped-tables-require-where`, `tenant-write-must-carry-tenant-id` | Rules whose model option must equal the scoped models. |
|
|
73
|
+
| `modelsOption` | `string` | `'tenantModels'` | The option those rules take the model list in. |
|
|
74
|
+
| `handScopedRule` | `string \| null` | `'noctcore-prisma/tenant-scoped-tables-require-where'` | The rule whose hand-scope option must equal `registry.handScopedModels`; `null` skips it. Checked only when the registry has hand-scoped models. |
|
|
75
|
+
| `handScopedOption` | `string` | `'handScopedModels'` | The option that rule takes the map in. |
|
|
76
|
+
| `configFile` | `string` | `cwd` | Path reported for config-side violations: where a maintainer edits the list. |
|
|
77
|
+
|
|
78
|
+
Keys are Prisma delegate accessors (`invoice` for `model Invoice`).
|
|
79
|
+
|
|
80
|
+
## Worked example: Settly
|
|
81
|
+
|
|
82
|
+
Settly scopes on `tenantId`, splits its schema per domain, keeps the runtime map in its API's tenant
|
|
83
|
+
extension and the exemption maps in a registry module its ESLint config also reads:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { createTenantModelRegistryParityRule } from '@noctcore/lint-meta-rules/prisma';
|
|
87
|
+
|
|
88
|
+
import { HAND_SCOPED_MODELS, HAND_SCOPE_PENDING, UNSCOPED_BY_DESIGN } from './tenant-registry';
|
|
89
|
+
|
|
90
|
+
createTenantModelRegistryParityRule({
|
|
91
|
+
schemaPath: 'packages/database/prisma/schema',
|
|
92
|
+
tenantFields: ['tenantId'],
|
|
93
|
+
scopedModelsSource: {
|
|
94
|
+
file: 'apps/api/src/common/database/tenant-scope.extension.ts',
|
|
95
|
+
exportName: 'TENANT_SCOPED_MODELS',
|
|
96
|
+
},
|
|
97
|
+
registry: {
|
|
98
|
+
scopedModels: [],
|
|
99
|
+
unscopedByDesign: UNSCOPED_BY_DESIGN,
|
|
100
|
+
handScopedModels: HAND_SCOPED_MODELS,
|
|
101
|
+
handScopePending: HAND_SCOPE_PENDING,
|
|
102
|
+
},
|
|
103
|
+
eslint: {
|
|
104
|
+
cwd: 'apps/api',
|
|
105
|
+
probe: 'src/modules/example/example.service.ts',
|
|
106
|
+
configFile: 'packages/eslint-config/nestjs.js',
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The registry is data the lint-meta registry imports from wherever the project keeps it; the rule
|
|
112
|
+
never reads a project file by a hardcoded path.
|
|
113
|
+
|
|
114
|
+
## When not to use it
|
|
115
|
+
|
|
116
|
+
A project with row-level security as its tenant boundary, or with no client-side scope map, has no
|
|
117
|
+
runtime registry to reconcile: use `eslint: false` and the schema half alone, or skip the rule.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noctcore/lint-meta-rules",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Portable, parameterized lint-meta rules — whole-repo / cross-file invariants ESLint cannot reach — for the @noctcore/harness lint-meta runner.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -18,6 +18,16 @@
|
|
|
18
18
|
"import": "./dist/i18n.js",
|
|
19
19
|
"require": "./dist/i18n.cjs"
|
|
20
20
|
},
|
|
21
|
+
"./prisma": {
|
|
22
|
+
"types": "./dist/prisma.d.ts",
|
|
23
|
+
"import": "./dist/prisma.js",
|
|
24
|
+
"require": "./dist/prisma.cjs"
|
|
25
|
+
},
|
|
26
|
+
"./resolved-config": {
|
|
27
|
+
"types": "./dist/resolved-config.d.ts",
|
|
28
|
+
"import": "./dist/resolved-config.js",
|
|
29
|
+
"require": "./dist/resolved-config.cjs"
|
|
30
|
+
},
|
|
21
31
|
"./package.json": "./package.json"
|
|
22
32
|
},
|
|
23
33
|
"files": [
|
|
@@ -46,12 +56,13 @@
|
|
|
46
56
|
"homepage": "https://github.com/noctcore/eslint-plugins/tree/main/packages/lint-meta-rules",
|
|
47
57
|
"bugs": "https://github.com/noctcore/eslint-plugins/issues",
|
|
48
58
|
"scripts": {
|
|
49
|
-
"build": "tsup src/index.ts src/i18n.ts --format esm,cjs --dts --clean",
|
|
59
|
+
"build": "tsup src/index.ts src/i18n.ts src/prisma.ts src/resolved-config.ts --format esm,cjs --dts --clean",
|
|
50
60
|
"typecheck": "tsc --noEmit",
|
|
51
61
|
"test": "bun test"
|
|
52
62
|
},
|
|
53
63
|
"dependencies": {
|
|
54
64
|
"@noctcore/eslint-plugin-contracts": "^0.5.0",
|
|
65
|
+
"@noctcore/eslint-plugin-prisma": "^0.3.0",
|
|
55
66
|
"@noctcore/harness": "^0.3.0",
|
|
56
67
|
"@typescript-eslint/utils": "^8.61.1"
|
|
57
68
|
},
|