@vibeorm/scoped-conformance 0.1.0-alpha.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 +81 -0
- package/dist/fixture.d.ts +116 -0
- package/dist/fixture.d.ts.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +766 -0
- package/dist/index.js.map +12 -0
- package/dist/vectors.d.ts +55 -0
- package/dist/vectors.d.ts.map +1 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# @vibeorm/scoped-conformance
|
|
2
|
+
|
|
3
|
+
The binding-agnostic conformance artifact for the **tenant-scoped database handle** — one shared
|
|
4
|
+
contract, two bindings:
|
|
5
|
+
|
|
6
|
+
1. **VibeORM** — first-class `$scoped` on the client (this repository; see `docs/queries.md`,
|
|
7
|
+
"Tenant scoping").
|
|
8
|
+
2. **Prisma** — a client-extension package (`$extends` query hooks) in pekka/shared.
|
|
9
|
+
|
|
10
|
+
Both bindings consume THIS package: the same fixture schema, seed, tenancy map, rewrite vectors
|
|
11
|
+
and behavior checklist — so the contract cannot drift between them.
|
|
12
|
+
|
|
13
|
+
## The contract (normative)
|
|
14
|
+
|
|
15
|
+
Input: base client + per-model tenancy map + scope value:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
base.$scoped({
|
|
19
|
+
scope: "ws_123", // string | null
|
|
20
|
+
models: {
|
|
21
|
+
Call: { column: "tenantId" },
|
|
22
|
+
Currency: "none", // global — passes through untouched
|
|
23
|
+
Leg: { through: "call" }, // tenant lives on the parent
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Behavior per call shape (scoped models; `"none"` models pass through):
|
|
29
|
+
|
|
30
|
+
- `findMany`/`findFirst`/`count`/`aggregate`/`groupBy`: scope predicate AND-merged into `where`.
|
|
31
|
+
- `findUnique`: REWRITTEN to `findFirst` with `{ ...uniqueWhere, [column]: scope }`; foreign row → `null`.
|
|
32
|
+
- `create`/`createMany`: `[column]: scope` auto-filled; explicit DIFFERENT value throws.
|
|
33
|
+
- `update`/`delete` (unique where): compare-and-write — the predicate joins the write's WHERE;
|
|
34
|
+
a foreign row behaves exactly like a missing one (`null`/not-found sentinel); never touches
|
|
35
|
+
another tenant's row.
|
|
36
|
+
- `upsert`, nested writes (`create`/`update`/`delete`/`connect`/`connectOrCreate`/`disconnect`/`set`),
|
|
37
|
+
relation filters in `where`/`orderBy`, `include`/`select` nesting, `_count`, `having`:
|
|
38
|
+
DENY-BY-DEFAULT over the whole query AST — every node touching a scoped model gets the
|
|
39
|
+
predicate injected where semantics allow, else the call REFUSES at build time with a typed
|
|
40
|
+
error naming model + node path. Unknown argument shapes REFUSE. Nested `connect` on a scoped
|
|
41
|
+
model: verified-lookup rewrite (the predicate joins the resolution where, inside the write's
|
|
42
|
+
own transaction) — per-model `nested: "refuse"` disables reference verbs instead.
|
|
43
|
+
- `$transaction` (both forms): the tx handle INHERITS the scope.
|
|
44
|
+
- Raw SQL (`$queryRaw`/`$executeRaw`/unsafe): REFUSED on the scoped handle (typed error pointing
|
|
45
|
+
at the unscoped escape — the BASE client). No `$raw`-with-tenant-param tier exists.
|
|
46
|
+
|
|
47
|
+
Rules:
|
|
48
|
+
|
|
49
|
+
- `scope: null` → every scoped-model call refuses (`"none"` models still pass).
|
|
50
|
+
- NULL tenant columns: the scoped handle matches ONLY `column = scope`. NULL rows (unassigned
|
|
51
|
+
inventory) are invisible to all scoped handles, reachable only via the base client. Scoped
|
|
52
|
+
create with explicit `null` throws.
|
|
53
|
+
- `{ through }`: supported where the binding's filter language expresses the join predicate;
|
|
54
|
+
shapes that cannot be expressed refuse instead (each binding documents its list).
|
|
55
|
+
- One typed error class: `ScopedQueryError { model, path, reason }` — matched structurally on
|
|
56
|
+
`name === "ScopedQueryError"` plus the triple, NEVER on message text or `instanceof`.
|
|
57
|
+
- The scoped handle's TYPE equals the base client's (minus raw methods) — app code needs zero
|
|
58
|
+
changes.
|
|
59
|
+
- Scope MAY later become a map of named axes (`{ owner: "c_1", partner: "m_2" }`); the single
|
|
60
|
+
axis stays the simple default spelling. Not built yet — do not design it away.
|
|
61
|
+
|
|
62
|
+
## What this package ships
|
|
63
|
+
|
|
64
|
+
| Export | What it is |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `SCOPED_FIXTURE_PRISMA` / `SCOPED_FIXTURE_IR` | The canonical fixture schema (text + neutral structural IR): `Call { column }`, `Leg { through }`, `Currency "none"` (nullable to-one INTO a scoped model), `Draft` (nullable tenant column), `Tag` (implicit M2M with a scoped model) |
|
|
67
|
+
| `SCOPED_MODELS` / `SCOPED_MODELS_LEG_REFUSE` | The canonical tenancy map (+ the strict-nested variant) |
|
|
68
|
+
| `SCOPED_SEED` / `TENANTS` | Two-tenant seed incl. a NULL-tenant row and a cross-tenant to-one |
|
|
69
|
+
| `REWRITE_VECTORS` | Pure vectors: `{ models, scope, call }` → exact rewritten call \| passthrough \| refusal `{ model, path, reason }` |
|
|
70
|
+
| `BEHAVIOR_CHECKLIST` | Named live-database scenarios every binding's suite must implement (test names cite the ids) |
|
|
71
|
+
| `describeRefusal` / `refusalMatches` | Structural refusal matching helpers |
|
|
72
|
+
|
|
73
|
+
## How a binding proves conformance
|
|
74
|
+
|
|
75
|
+
1. **Rewrite tier**: run every `REWRITE_VECTORS` entry through the binding's pure rewriter and
|
|
76
|
+
compare args byte-for-byte (VibeORM: `createScopeRewriter` in `@vibeorm/runtime`, exercised in
|
|
77
|
+
`packages/runtime/tests/scoped.test.ts`).
|
|
78
|
+
2. **Behavior tier**: implement every `BEHAVIOR_CHECKLIST` id as a live test against the fixture +
|
|
79
|
+
seed (VibeORM: `apps/tests/src/scoped/scoped.e2e.test.ts`, dialect-parameterized).
|
|
80
|
+
|
|
81
|
+
A binding is conformant when both tiers are green and no checklist id is unimplemented.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The canonical conformance fixture: one small schema exercising every
|
|
3
|
+
* tenancy shape the $scoped contract names —
|
|
4
|
+
* Call { column: "workspaceId" } tenant column on the row
|
|
5
|
+
* Leg { through: "call" } tenant lives on the to-one parent
|
|
6
|
+
* Currency "none" global reference data (with a
|
|
7
|
+
* nullable to-one INTO a scoped model)
|
|
8
|
+
* Draft { column: "workspaceId" } NULLABLE tenant column (NULL rows
|
|
9
|
+
* are invisible to every scoped handle)
|
|
10
|
+
* Tag "none" implicit M2M with a scoped model
|
|
11
|
+
*
|
|
12
|
+
* Shipped in two equivalent forms: Prisma schema text (for bindings that
|
|
13
|
+
* parse) and a neutral structural IR (for bindings that build metadata
|
|
14
|
+
* directly). This package has NO dependencies — the IR types below are
|
|
15
|
+
* structural mirrors, not imports.
|
|
16
|
+
*/
|
|
17
|
+
export type FixtureField = {
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly type: string | {
|
|
20
|
+
readonly enum: string;
|
|
21
|
+
};
|
|
22
|
+
readonly isList: boolean;
|
|
23
|
+
readonly isOptional: boolean;
|
|
24
|
+
readonly isId: boolean;
|
|
25
|
+
readonly isUnique: boolean;
|
|
26
|
+
readonly isUpdatedAt: boolean;
|
|
27
|
+
readonly default?: {
|
|
28
|
+
readonly kind: string;
|
|
29
|
+
readonly value?: unknown;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
export type FixtureRelation = {
|
|
33
|
+
readonly name: string;
|
|
34
|
+
readonly model: string;
|
|
35
|
+
readonly targetModel: string;
|
|
36
|
+
readonly type: "oneToOne" | "oneToMany" | "manyToOne" | "manyToMany";
|
|
37
|
+
readonly relationName?: string;
|
|
38
|
+
readonly fields?: readonly string[];
|
|
39
|
+
readonly references?: readonly string[];
|
|
40
|
+
readonly isList: boolean;
|
|
41
|
+
readonly isOptional: boolean;
|
|
42
|
+
};
|
|
43
|
+
export type FixtureModel = {
|
|
44
|
+
readonly name: string;
|
|
45
|
+
readonly fields: readonly FixtureField[];
|
|
46
|
+
readonly relations: readonly FixtureRelation[];
|
|
47
|
+
readonly indexes: readonly unknown[];
|
|
48
|
+
readonly isView: boolean;
|
|
49
|
+
};
|
|
50
|
+
export type FixtureSchema = {
|
|
51
|
+
readonly datasource: {
|
|
52
|
+
readonly provider: string;
|
|
53
|
+
};
|
|
54
|
+
readonly enums: readonly unknown[];
|
|
55
|
+
readonly extensions: readonly unknown[];
|
|
56
|
+
readonly models: readonly FixtureModel[];
|
|
57
|
+
};
|
|
58
|
+
/** The fixture as Prisma schema text (datasource provider is up to the harness). */
|
|
59
|
+
export declare const SCOPED_FIXTURE_PRISMA: string;
|
|
60
|
+
/** The same fixture as neutral structural IR. */
|
|
61
|
+
export declare const SCOPED_FIXTURE_IR: FixtureSchema;
|
|
62
|
+
export type ConformanceNestedMode = "verify" | "refuse";
|
|
63
|
+
export type ConformanceModelEntry = "none" | {
|
|
64
|
+
readonly column: string;
|
|
65
|
+
readonly nested?: ConformanceNestedMode;
|
|
66
|
+
} | {
|
|
67
|
+
readonly through: string;
|
|
68
|
+
readonly nested?: ConformanceNestedMode;
|
|
69
|
+
};
|
|
70
|
+
export type ConformanceModelsMap = Readonly<Record<string, ConformanceModelEntry>>;
|
|
71
|
+
/** The default tenancy map every vector assumes unless it carries its own. */
|
|
72
|
+
export declare const SCOPED_MODELS: ConformanceModelsMap;
|
|
73
|
+
/** Variant map with strict nested mode on Leg (the refuse-mode vectors use it). */
|
|
74
|
+
export declare const SCOPED_MODELS_LEG_REFUSE: ConformanceModelsMap;
|
|
75
|
+
/** The two tenants every behavior case runs against. */
|
|
76
|
+
export declare const TENANTS: {
|
|
77
|
+
readonly a: string;
|
|
78
|
+
readonly b: string;
|
|
79
|
+
};
|
|
80
|
+
export type SeedSpec = {
|
|
81
|
+
readonly calls: ReadonlyArray<{
|
|
82
|
+
readonly id: string;
|
|
83
|
+
readonly workspaceId: string;
|
|
84
|
+
readonly slug: string;
|
|
85
|
+
readonly title: string;
|
|
86
|
+
readonly secret: string;
|
|
87
|
+
}>;
|
|
88
|
+
readonly legs: ReadonlyArray<{
|
|
89
|
+
readonly id: string;
|
|
90
|
+
readonly callId: string;
|
|
91
|
+
readonly duration: number;
|
|
92
|
+
}>;
|
|
93
|
+
readonly currencies: ReadonlyArray<{
|
|
94
|
+
readonly code: string;
|
|
95
|
+
readonly name: string;
|
|
96
|
+
readonly featuredCallId: string | null;
|
|
97
|
+
}>;
|
|
98
|
+
readonly drafts: ReadonlyArray<{
|
|
99
|
+
readonly id: string;
|
|
100
|
+
readonly workspaceId: string | null;
|
|
101
|
+
readonly title: string;
|
|
102
|
+
}>;
|
|
103
|
+
readonly tags: ReadonlyArray<{
|
|
104
|
+
readonly id: string;
|
|
105
|
+
readonly name: string;
|
|
106
|
+
readonly callIds: readonly string[];
|
|
107
|
+
}>;
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Canonical seed: tenant A owns c1 (legs l1, l2), tenant B owns c2 (leg l3);
|
|
111
|
+
* USD features tenant B's call (the cross-tenant to-one every oracle vector
|
|
112
|
+
* probes), NOK features tenant A's, EUR features none; d0 has a NULL tenant
|
|
113
|
+
* (invisible to every scoped handle); tag t1 links BOTH tenants' calls.
|
|
114
|
+
*/
|
|
115
|
+
export declare const SCOPED_SEED: SeedSpec;
|
|
116
|
+
//# sourceMappingURL=fixture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixture.d.ts","sourceRoot":"","sources":["../src/fixture.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vibeorm/scoped-conformance — the binding-agnostic conformance artifact for
|
|
3
|
+
* the tenant-scoped database handle (`$scoped` in VibeORM, the Prisma
|
|
4
|
+
* client-extension binding in pekka/shared).
|
|
5
|
+
*
|
|
6
|
+
* Dependency-free by design: both bindings consume the SAME fixture, seed,
|
|
7
|
+
* tenancy map and vectors, so the contract cannot drift between them.
|
|
8
|
+
*/
|
|
9
|
+
export { SCOPED_FIXTURE_IR, SCOPED_FIXTURE_PRISMA, SCOPED_MODELS, SCOPED_MODELS_LEG_REFUSE, SCOPED_SEED, TENANTS, } from "./fixture.ts";
|
|
10
|
+
export type { ConformanceModelEntry, ConformanceModelsMap, ConformanceNestedMode, FixtureField, FixtureModel, FixtureRelation, FixtureSchema, SeedSpec, } from "./fixture.ts";
|
|
11
|
+
export { BEHAVIOR_CHECKLIST, REWRITE_VECTORS } from "./vectors.ts";
|
|
12
|
+
export type { BehaviorCase, ExpectedRefusal, RewriteExpectation, RewriteVector, ScopedCall, } from "./vectors.ts";
|
|
13
|
+
/**
|
|
14
|
+
* Read the `{ model, path, reason }` triple off a binding's refusal error.
|
|
15
|
+
* Matches structurally (never `instanceof` — each binding ships its own
|
|
16
|
+
* `ScopedQueryError` class with these fields and `name: "ScopedQueryError"`).
|
|
17
|
+
*/
|
|
18
|
+
export declare function describeRefusal(error: unknown): {
|
|
19
|
+
model: string;
|
|
20
|
+
path: string;
|
|
21
|
+
reason: string;
|
|
22
|
+
} | null;
|
|
23
|
+
/** Does a caught error match a vector's expected refusal? */
|
|
24
|
+
export declare function refusalMatches(params: {
|
|
25
|
+
expected: {
|
|
26
|
+
model: string;
|
|
27
|
+
path: string;
|
|
28
|
+
reason: string;
|
|
29
|
+
};
|
|
30
|
+
error: unknown;
|
|
31
|
+
}): boolean;
|
|
32
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,aAAa,EACb,wBAAwB,EACxB,WAAW,EACX,OAAO,GACR,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,qBAAqB,EACrB,oBAAoB,EACpB,qBAAqB,EACrB,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,aAAa,EACb,QAAQ,GACT,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACnE,YAAY,EACV,YAAY,EACZ,eAAe,EACf,kBAAkB,EAClB,aAAa,EACb,UAAU,GACX,MAAM,cAAc,CAAC;AAEtB;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAQtG;AAED,6DAA6D;AAC7D,wBAAgB,cAAc,CAAC,MAAM,EAAE;IAAE,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAQ7H"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/scoped-conformance/src/fixture.ts
|
|
3
|
+
function scalar(params) {
|
|
4
|
+
return {
|
|
5
|
+
name: params.name,
|
|
6
|
+
type: params.type,
|
|
7
|
+
isList: false,
|
|
8
|
+
isOptional: params.optional === true,
|
|
9
|
+
isId: params.id === true,
|
|
10
|
+
isUnique: params.unique === true,
|
|
11
|
+
isUpdatedAt: false
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
var SCOPED_FIXTURE_PRISMA = `
|
|
15
|
+
datasource db {
|
|
16
|
+
provider = "postgresql"
|
|
17
|
+
url = env("DATABASE_URL")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
model Call {
|
|
21
|
+
id String @id
|
|
22
|
+
workspaceId String
|
|
23
|
+
slug String @unique
|
|
24
|
+
title String
|
|
25
|
+
secret String
|
|
26
|
+
legs Leg[]
|
|
27
|
+
featured Currency[]
|
|
28
|
+
tags Tag[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
model Leg {
|
|
32
|
+
id String @id
|
|
33
|
+
callId String
|
|
34
|
+
call Call @relation(fields: [callId], references: [id])
|
|
35
|
+
duration Int
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
model Currency {
|
|
39
|
+
code String @id
|
|
40
|
+
name String
|
|
41
|
+
featuredCallId String?
|
|
42
|
+
featuredCall Call? @relation(fields: [featuredCallId], references: [id])
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
model Draft {
|
|
46
|
+
id String @id
|
|
47
|
+
workspaceId String?
|
|
48
|
+
title String
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
model Tag {
|
|
52
|
+
id String @id
|
|
53
|
+
name String @unique
|
|
54
|
+
calls Call[]
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
57
|
+
var SCOPED_FIXTURE_IR = {
|
|
58
|
+
datasource: { provider: "postgresql" },
|
|
59
|
+
enums: [],
|
|
60
|
+
extensions: [],
|
|
61
|
+
models: [
|
|
62
|
+
{
|
|
63
|
+
name: "Call",
|
|
64
|
+
fields: [
|
|
65
|
+
scalar({ name: "id", type: "String", id: true }),
|
|
66
|
+
scalar({ name: "workspaceId", type: "String" }),
|
|
67
|
+
scalar({ name: "slug", type: "String", unique: true }),
|
|
68
|
+
scalar({ name: "title", type: "String" }),
|
|
69
|
+
scalar({ name: "secret", type: "String" })
|
|
70
|
+
],
|
|
71
|
+
relations: [
|
|
72
|
+
{ name: "legs", model: "Call", targetModel: "Leg", type: "oneToMany", isList: true, isOptional: false },
|
|
73
|
+
{ name: "featured", model: "Call", targetModel: "Currency", type: "oneToMany", isList: true, isOptional: false },
|
|
74
|
+
{ name: "tags", model: "Call", targetModel: "Tag", type: "manyToMany", isList: true, isOptional: false }
|
|
75
|
+
],
|
|
76
|
+
indexes: [],
|
|
77
|
+
isView: false
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "Leg",
|
|
81
|
+
fields: [
|
|
82
|
+
scalar({ name: "id", type: "String", id: true }),
|
|
83
|
+
scalar({ name: "callId", type: "String" }),
|
|
84
|
+
scalar({ name: "duration", type: "Int" })
|
|
85
|
+
],
|
|
86
|
+
relations: [
|
|
87
|
+
{
|
|
88
|
+
name: "call",
|
|
89
|
+
model: "Leg",
|
|
90
|
+
targetModel: "Call",
|
|
91
|
+
type: "manyToOne",
|
|
92
|
+
fields: ["callId"],
|
|
93
|
+
references: ["id"],
|
|
94
|
+
isList: false,
|
|
95
|
+
isOptional: false
|
|
96
|
+
}
|
|
97
|
+
],
|
|
98
|
+
indexes: [],
|
|
99
|
+
isView: false
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "Currency",
|
|
103
|
+
fields: [
|
|
104
|
+
scalar({ name: "code", type: "String", id: true }),
|
|
105
|
+
scalar({ name: "name", type: "String" }),
|
|
106
|
+
scalar({ name: "featuredCallId", type: "String", optional: true })
|
|
107
|
+
],
|
|
108
|
+
relations: [
|
|
109
|
+
{
|
|
110
|
+
name: "featuredCall",
|
|
111
|
+
model: "Currency",
|
|
112
|
+
targetModel: "Call",
|
|
113
|
+
type: "manyToOne",
|
|
114
|
+
fields: ["featuredCallId"],
|
|
115
|
+
references: ["id"],
|
|
116
|
+
isList: false,
|
|
117
|
+
isOptional: true
|
|
118
|
+
}
|
|
119
|
+
],
|
|
120
|
+
indexes: [],
|
|
121
|
+
isView: false
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: "Draft",
|
|
125
|
+
fields: [
|
|
126
|
+
scalar({ name: "id", type: "String", id: true }),
|
|
127
|
+
scalar({ name: "workspaceId", type: "String", optional: true }),
|
|
128
|
+
scalar({ name: "title", type: "String" })
|
|
129
|
+
],
|
|
130
|
+
relations: [],
|
|
131
|
+
indexes: [],
|
|
132
|
+
isView: false
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: "Tag",
|
|
136
|
+
fields: [scalar({ name: "id", type: "String", id: true }), scalar({ name: "name", type: "String", unique: true })],
|
|
137
|
+
relations: [
|
|
138
|
+
{ name: "calls", model: "Tag", targetModel: "Call", type: "manyToMany", isList: true, isOptional: false }
|
|
139
|
+
],
|
|
140
|
+
indexes: [],
|
|
141
|
+
isView: false
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
};
|
|
145
|
+
var SCOPED_MODELS = {
|
|
146
|
+
Call: { column: "workspaceId" },
|
|
147
|
+
Leg: { through: "call" },
|
|
148
|
+
Currency: "none",
|
|
149
|
+
Draft: { column: "workspaceId" },
|
|
150
|
+
Tag: "none"
|
|
151
|
+
};
|
|
152
|
+
var SCOPED_MODELS_LEG_REFUSE = {
|
|
153
|
+
...SCOPED_MODELS,
|
|
154
|
+
Leg: { through: "call", nested: "refuse" }
|
|
155
|
+
};
|
|
156
|
+
var TENANTS = { a: "ws_1", b: "ws_2" };
|
|
157
|
+
var SCOPED_SEED = {
|
|
158
|
+
calls: [
|
|
159
|
+
{ id: "c1", workspaceId: TENANTS.a, slug: "call-one", title: "standup", secret: "alpha-secret" },
|
|
160
|
+
{ id: "c2", workspaceId: TENANTS.b, slug: "call-two", title: "standup", secret: "beta-secret" }
|
|
161
|
+
],
|
|
162
|
+
legs: [
|
|
163
|
+
{ id: "l1", callId: "c1", duration: 60 },
|
|
164
|
+
{ id: "l2", callId: "c1", duration: 120 },
|
|
165
|
+
{ id: "l3", callId: "c2", duration: 240 }
|
|
166
|
+
],
|
|
167
|
+
currencies: [
|
|
168
|
+
{ code: "USD", name: "US Dollar", featuredCallId: "c2" },
|
|
169
|
+
{ code: "NOK", name: "Norwegian Krone", featuredCallId: "c1" },
|
|
170
|
+
{ code: "EUR", name: "Euro", featuredCallId: null }
|
|
171
|
+
],
|
|
172
|
+
drafts: [
|
|
173
|
+
{ id: "d1", workspaceId: TENANTS.a, title: "mine" },
|
|
174
|
+
{ id: "d2", workspaceId: TENANTS.b, title: "theirs" },
|
|
175
|
+
{ id: "d0", workspaceId: null, title: "unassigned" }
|
|
176
|
+
],
|
|
177
|
+
tags: [{ id: "t1", name: "shared", callIds: ["c1", "c2"] }]
|
|
178
|
+
};
|
|
179
|
+
// packages/scoped-conformance/src/vectors.ts
|
|
180
|
+
var A = TENANTS.a;
|
|
181
|
+
var B = TENANTS.b;
|
|
182
|
+
var predCall = { workspaceId: A };
|
|
183
|
+
var predLeg = { call: { workspaceId: A } };
|
|
184
|
+
var CALL_CREATE_DATA = { id: "c9", slug: "call-nine", title: "retro", secret: "s9" };
|
|
185
|
+
var REWRITE_VECTORS = [
|
|
186
|
+
{
|
|
187
|
+
name: "find-merge",
|
|
188
|
+
description: "findMany AND-merges the tenant predicate into the user's where",
|
|
189
|
+
scope: A,
|
|
190
|
+
call: { model: "Call", method: "findMany", args: { where: { title: "standup" } } },
|
|
191
|
+
expected: {
|
|
192
|
+
kind: "rewrite",
|
|
193
|
+
call: { model: "Call", method: "findMany", args: { where: { AND: [{ title: "standup" }, predCall] } } }
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: "find-no-where",
|
|
198
|
+
description: "findMany without a where gets the bare predicate",
|
|
199
|
+
scope: A,
|
|
200
|
+
call: { model: "Call", method: "findMany" },
|
|
201
|
+
expected: { kind: "rewrite", call: { model: "Call", method: "findMany", args: { where: predCall } } }
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: "none-passthrough",
|
|
205
|
+
description: '"none" models pass through untouched',
|
|
206
|
+
scope: A,
|
|
207
|
+
call: { model: "Currency", method: "findMany", args: { where: { name: "Euro" } } },
|
|
208
|
+
expected: { kind: "passthrough" }
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
name: "findunique-rewrite",
|
|
212
|
+
description: "findUnique rewrites to findFirst with the unique key + predicate; a foreign row reads as null",
|
|
213
|
+
scope: A,
|
|
214
|
+
call: { model: "Call", method: "findUnique", args: { where: { id: "c1" } } },
|
|
215
|
+
expected: {
|
|
216
|
+
kind: "rewrite",
|
|
217
|
+
call: { model: "Call", method: "findFirst", args: { where: { AND: [{ id: "c1" }, predCall] } } }
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: "findunique-none-kept",
|
|
222
|
+
description: 'findUnique on a "none" model stays findUnique',
|
|
223
|
+
scope: A,
|
|
224
|
+
call: { model: "Currency", method: "findUnique", args: { where: { code: "USD" } } },
|
|
225
|
+
expected: { kind: "passthrough" }
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: "through-find",
|
|
229
|
+
description: "a { through } model filters via its tenancy parent",
|
|
230
|
+
scope: A,
|
|
231
|
+
call: { model: "Leg", method: "findMany" },
|
|
232
|
+
expected: { kind: "rewrite", call: { model: "Leg", method: "findMany", args: { where: predLeg } } }
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: "count-merge",
|
|
236
|
+
description: "count AND-merges the predicate",
|
|
237
|
+
scope: A,
|
|
238
|
+
call: { model: "Call", method: "count", args: { where: { title: "standup" } } },
|
|
239
|
+
expected: {
|
|
240
|
+
kind: "rewrite",
|
|
241
|
+
call: { model: "Call", method: "count", args: { where: { AND: [{ title: "standup" }, predCall] } } }
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
name: "aggregate-merge",
|
|
246
|
+
description: "aggregate gets the predicate",
|
|
247
|
+
scope: A,
|
|
248
|
+
call: { model: "Call", method: "aggregate", args: { _count: true } },
|
|
249
|
+
expected: {
|
|
250
|
+
kind: "rewrite",
|
|
251
|
+
call: { model: "Call", method: "aggregate", args: { _count: true, where: predCall } }
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: "groupby-merge",
|
|
256
|
+
description: "groupBy gets the predicate",
|
|
257
|
+
scope: A,
|
|
258
|
+
call: { model: "Call", method: "groupBy", args: { by: ["title"], _count: true } },
|
|
259
|
+
expected: {
|
|
260
|
+
kind: "rewrite",
|
|
261
|
+
call: { model: "Call", method: "groupBy", args: { by: ["title"], _count: true, where: predCall } }
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: "relation-is-inject",
|
|
266
|
+
description: "a to-one relation filter into a scoped model gets the predicate (bare object = is)",
|
|
267
|
+
scope: A,
|
|
268
|
+
call: { model: "Currency", method: "findMany", args: { where: { featuredCall: { title: "standup" } } } },
|
|
269
|
+
expected: {
|
|
270
|
+
kind: "rewrite",
|
|
271
|
+
call: {
|
|
272
|
+
model: "Currency",
|
|
273
|
+
method: "findMany",
|
|
274
|
+
args: { where: { featuredCall: { is: { AND: [{ title: "standup" }, predCall] } } } }
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: "relation-is-null-flip",
|
|
280
|
+
description: "is: null means 'no VISIBLE parent' \u2014 a foreign parent reads as absent",
|
|
281
|
+
scope: A,
|
|
282
|
+
call: { model: "Currency", method: "findMany", args: { where: { featuredCall: null } } },
|
|
283
|
+
expected: {
|
|
284
|
+
kind: "rewrite",
|
|
285
|
+
call: { model: "Currency", method: "findMany", args: { where: { featuredCall: { isNot: predCall } } } }
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: "relation-isnot-null-flip",
|
|
290
|
+
description: "isNot: null means 'a VISIBLE parent exists'",
|
|
291
|
+
scope: A,
|
|
292
|
+
call: { model: "Currency", method: "findMany", args: { where: { featuredCall: { isNot: null } } } },
|
|
293
|
+
expected: {
|
|
294
|
+
kind: "rewrite",
|
|
295
|
+
call: { model: "Currency", method: "findMany", args: { where: { featuredCall: { is: predCall } } } }
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: "m2m-some-inject",
|
|
300
|
+
description: "a to-many (implicit M2M) filter into a scoped model gets the predicate \u2014 the cross-tenant existence oracle closes",
|
|
301
|
+
scope: A,
|
|
302
|
+
call: { model: "Tag", method: "findMany", args: { where: { calls: { some: { secret: "beta-secret" } } } } },
|
|
303
|
+
expected: {
|
|
304
|
+
kind: "rewrite",
|
|
305
|
+
call: {
|
|
306
|
+
model: "Tag",
|
|
307
|
+
method: "findMany",
|
|
308
|
+
args: { where: { calls: { some: { AND: [{ secret: "beta-secret" }, predCall] } } } }
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
name: "every-rewrite",
|
|
314
|
+
description: "every p becomes none(tenant AND NOT p): only VISIBLE children must satisfy p",
|
|
315
|
+
scope: A,
|
|
316
|
+
call: { model: "Call", method: "findMany", args: { where: { legs: { every: { duration: { gt: 100 } } } } } },
|
|
317
|
+
expected: {
|
|
318
|
+
kind: "rewrite",
|
|
319
|
+
call: {
|
|
320
|
+
model: "Call",
|
|
321
|
+
method: "findMany",
|
|
322
|
+
args: {
|
|
323
|
+
where: {
|
|
324
|
+
AND: [{ legs: { none: { AND: [predLeg, { NOT: [{ duration: { gt: 100 } }] }] } } }, predCall]
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
name: "orderby-refuse",
|
|
332
|
+
description: "orderBy through a relation onto a scoped model refuses (ordering would leak foreign values)",
|
|
333
|
+
scope: A,
|
|
334
|
+
call: { model: "Currency", method: "findMany", args: { orderBy: { featuredCall: { title: "asc" } } } },
|
|
335
|
+
expected: {
|
|
336
|
+
kind: "refuse",
|
|
337
|
+
refusal: { model: "Call", path: "currency.findMany.orderBy.featuredCall", reason: "relation_orderby_on_scoped" }
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: "orderby-tenancy-edge",
|
|
342
|
+
description: "a through model may order by its OWN tenancy parent (in-tenant by definition)",
|
|
343
|
+
scope: A,
|
|
344
|
+
call: { model: "Leg", method: "findMany", args: { orderBy: { call: { title: "asc" } } } },
|
|
345
|
+
expected: {
|
|
346
|
+
kind: "rewrite",
|
|
347
|
+
call: { model: "Leg", method: "findMany", args: { orderBy: { call: { title: "asc" } }, where: predLeg } }
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
name: "include-inject",
|
|
352
|
+
description: "include of a scoped child gets the predicate in its relation args",
|
|
353
|
+
scope: A,
|
|
354
|
+
call: { model: "Call", method: "findMany", args: { include: { legs: true } } },
|
|
355
|
+
expected: {
|
|
356
|
+
kind: "rewrite",
|
|
357
|
+
call: { model: "Call", method: "findMany", args: { include: { legs: { where: predLeg } }, where: predCall } }
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
name: "include-child-where",
|
|
362
|
+
description: "a child where AND-merges with the predicate",
|
|
363
|
+
scope: A,
|
|
364
|
+
call: { model: "Call", method: "findMany", args: { include: { legs: { where: { duration: { gt: 60 } } } } } },
|
|
365
|
+
expected: {
|
|
366
|
+
kind: "rewrite",
|
|
367
|
+
call: {
|
|
368
|
+
model: "Call",
|
|
369
|
+
method: "findMany",
|
|
370
|
+
args: { include: { legs: { where: { AND: [{ duration: { gt: 60 } }, predLeg] } } }, where: predCall }
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
name: "include-none-pass",
|
|
376
|
+
description: 'including a "none" child passes through',
|
|
377
|
+
scope: A,
|
|
378
|
+
call: { model: "Call", method: "findMany", args: { include: { featured: true } } },
|
|
379
|
+
expected: {
|
|
380
|
+
kind: "rewrite",
|
|
381
|
+
call: { model: "Call", method: "findMany", args: { include: { featured: true }, where: predCall } }
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
name: "count-relation-refuse",
|
|
386
|
+
description: "_count of a scoped relation refuses (relation counts take no filter \u2014 it would count foreign rows)",
|
|
387
|
+
scope: A,
|
|
388
|
+
call: { model: "Tag", method: "findMany", args: { include: { _count: { select: { calls: true } } } } },
|
|
389
|
+
expected: {
|
|
390
|
+
kind: "refuse",
|
|
391
|
+
refusal: { model: "Call", path: "tag.findMany.include._count.calls", reason: "count_on_scoped_relation" }
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
name: "count-tenancy-edge-ok",
|
|
396
|
+
description: "_count over the tenancy inverse is allowed \u2014 those children ARE this row's tenant",
|
|
397
|
+
scope: A,
|
|
398
|
+
call: { model: "Call", method: "findMany", args: { include: { _count: { select: { legs: true } } } } },
|
|
399
|
+
expected: {
|
|
400
|
+
kind: "rewrite",
|
|
401
|
+
call: {
|
|
402
|
+
model: "Call",
|
|
403
|
+
method: "findMany",
|
|
404
|
+
args: { include: { _count: { select: { legs: true } } }, where: predCall }
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
name: "create-autofill",
|
|
410
|
+
description: "create fills the tenant column",
|
|
411
|
+
scope: A,
|
|
412
|
+
call: { model: "Call", method: "create", args: { data: { ...CALL_CREATE_DATA } } },
|
|
413
|
+
expected: {
|
|
414
|
+
kind: "rewrite",
|
|
415
|
+
call: { model: "Call", method: "create", args: { data: { ...CALL_CREATE_DATA, workspaceId: A } } }
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
name: "create-mismatch-refuse",
|
|
420
|
+
description: "an explicit DIFFERENT tenant value refuses before any SQL",
|
|
421
|
+
scope: A,
|
|
422
|
+
call: { model: "Call", method: "create", args: { data: { ...CALL_CREATE_DATA, workspaceId: B } } },
|
|
423
|
+
expected: {
|
|
424
|
+
kind: "refuse",
|
|
425
|
+
refusal: { model: "Call", path: "call.create.data.workspaceId", reason: "explicit_scope_mismatch" }
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
name: "create-null-refuse",
|
|
430
|
+
description: "an explicit NULL tenant value refuses (NULL rows are reachable only via the base client)",
|
|
431
|
+
scope: A,
|
|
432
|
+
call: { model: "Call", method: "create", args: { data: { ...CALL_CREATE_DATA, workspaceId: null } } },
|
|
433
|
+
expected: {
|
|
434
|
+
kind: "refuse",
|
|
435
|
+
refusal: { model: "Call", path: "call.create.data.workspaceId", reason: "explicit_scope_mismatch" }
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
name: "createmany-autofill",
|
|
440
|
+
description: "createMany fills every row",
|
|
441
|
+
scope: A,
|
|
442
|
+
call: {
|
|
443
|
+
model: "Draft",
|
|
444
|
+
method: "createMany",
|
|
445
|
+
args: { data: [{ id: "d8", title: "a" }, { id: "d9", title: "b" }] }
|
|
446
|
+
},
|
|
447
|
+
expected: {
|
|
448
|
+
kind: "rewrite",
|
|
449
|
+
call: {
|
|
450
|
+
model: "Draft",
|
|
451
|
+
method: "createMany",
|
|
452
|
+
args: { data: [{ id: "d8", title: "a", workspaceId: A }, { id: "d9", title: "b", workspaceId: A }] }
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
name: "nested-create-tenancy-edge",
|
|
458
|
+
description: "nested create under the tenancy inverse passes structurally (the parent defines the child's tenant)",
|
|
459
|
+
scope: A,
|
|
460
|
+
call: {
|
|
461
|
+
model: "Call",
|
|
462
|
+
method: "create",
|
|
463
|
+
args: { data: { ...CALL_CREATE_DATA, legs: { create: [{ id: "l9", duration: 30 }] } } }
|
|
464
|
+
},
|
|
465
|
+
expected: {
|
|
466
|
+
kind: "rewrite",
|
|
467
|
+
call: {
|
|
468
|
+
model: "Call",
|
|
469
|
+
method: "create",
|
|
470
|
+
args: {
|
|
471
|
+
data: { ...CALL_CREATE_DATA, workspaceId: A, legs: { create: [{ id: "l9", duration: 30 }] } }
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
name: "nested-connect-inject",
|
|
478
|
+
description: "nested connect becomes a tenant-verified lookup (predicate joins the unique where)",
|
|
479
|
+
scope: A,
|
|
480
|
+
call: {
|
|
481
|
+
model: "Call",
|
|
482
|
+
method: "create",
|
|
483
|
+
args: { data: { ...CALL_CREATE_DATA, legs: { connect: [{ id: "l1" }] } } }
|
|
484
|
+
},
|
|
485
|
+
expected: {
|
|
486
|
+
kind: "rewrite",
|
|
487
|
+
call: {
|
|
488
|
+
model: "Call",
|
|
489
|
+
method: "create",
|
|
490
|
+
args: {
|
|
491
|
+
data: { ...CALL_CREATE_DATA, workspaceId: A, legs: { connect: [{ id: "l1", AND: [predLeg] }] } }
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
name: "nested-connect-refuse-mode",
|
|
498
|
+
description: 'nested: "refuse" rejects reference verbs on that model',
|
|
499
|
+
models: SCOPED_MODELS_LEG_REFUSE,
|
|
500
|
+
scope: A,
|
|
501
|
+
call: {
|
|
502
|
+
model: "Call",
|
|
503
|
+
method: "create",
|
|
504
|
+
args: { data: { ...CALL_CREATE_DATA, legs: { connect: [{ id: "l1" }] } } }
|
|
505
|
+
},
|
|
506
|
+
expected: {
|
|
507
|
+
kind: "refuse",
|
|
508
|
+
refusal: { model: "Leg", path: "call.create.data.legs.connect", reason: "nested_refused" }
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
name: "through-create-missing-parent",
|
|
513
|
+
description: "creating a through-scoped row without its tenancy parent refuses",
|
|
514
|
+
scope: A,
|
|
515
|
+
call: { model: "Leg", method: "create", args: { data: { id: "l9", duration: 5 } } },
|
|
516
|
+
expected: { kind: "refuse", refusal: { model: "Leg", path: "leg.create.data", reason: "through_fk_write" } }
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
name: "through-create-bare-fk",
|
|
520
|
+
description: "a bare foreign-key write on a through-scoped model refuses \u2014 the relation verbs are the verified path",
|
|
521
|
+
scope: A,
|
|
522
|
+
call: { model: "Leg", method: "create", args: { data: { id: "l9", duration: 5, callId: "c1" } } },
|
|
523
|
+
expected: {
|
|
524
|
+
kind: "refuse",
|
|
525
|
+
refusal: { model: "Leg", path: "leg.create.data.callId", reason: "through_fk_write" }
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
name: "through-create-connect",
|
|
530
|
+
description: "creating a through-scoped row via connect verifies the parent in-tenant",
|
|
531
|
+
scope: A,
|
|
532
|
+
call: {
|
|
533
|
+
model: "Leg",
|
|
534
|
+
method: "create",
|
|
535
|
+
args: { data: { id: "l9", duration: 5, call: { connect: { slug: "call-one" } } } }
|
|
536
|
+
},
|
|
537
|
+
expected: {
|
|
538
|
+
kind: "rewrite",
|
|
539
|
+
call: {
|
|
540
|
+
model: "Leg",
|
|
541
|
+
method: "create",
|
|
542
|
+
args: { data: { id: "l9", duration: 5, call: { connect: { slug: "call-one", AND: [predCall] } } } }
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
name: "update-inject",
|
|
548
|
+
description: "update injects the predicate into the unique where \u2014 compare-and-write, foreign row behaves like missing",
|
|
549
|
+
scope: A,
|
|
550
|
+
call: { model: "Call", method: "update", args: { where: { id: "c1" }, data: { title: "renamed" } } },
|
|
551
|
+
expected: {
|
|
552
|
+
kind: "rewrite",
|
|
553
|
+
call: {
|
|
554
|
+
model: "Call",
|
|
555
|
+
method: "update",
|
|
556
|
+
args: { where: { id: "c1", AND: [predCall] }, data: { title: "renamed" } }
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
name: "update-column-guard",
|
|
562
|
+
description: "update writing the tenant column refuses \u2014 a scoped handle cannot move rows between tenants",
|
|
563
|
+
scope: A,
|
|
564
|
+
call: { model: "Call", method: "update", args: { where: { id: "c1" }, data: { workspaceId: B } } },
|
|
565
|
+
expected: {
|
|
566
|
+
kind: "refuse",
|
|
567
|
+
refusal: { model: "Call", path: "call.update.data.workspaceId", reason: "explicit_scope_mismatch" }
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
name: "updatemany-merge",
|
|
572
|
+
description: "updateMany AND-merges; the count reflects only in-tenant rows",
|
|
573
|
+
scope: A,
|
|
574
|
+
call: { model: "Call", method: "updateMany", args: { where: { title: "standup" }, data: { secret: "s" } } },
|
|
575
|
+
expected: {
|
|
576
|
+
kind: "rewrite",
|
|
577
|
+
call: {
|
|
578
|
+
model: "Call",
|
|
579
|
+
method: "updateMany",
|
|
580
|
+
args: { where: { AND: [{ title: "standup" }, predCall] }, data: { secret: "s" } }
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
name: "delete-inject",
|
|
586
|
+
description: "delete injects the predicate \u2014 a foreign row behaves like a missing one",
|
|
587
|
+
scope: A,
|
|
588
|
+
call: { model: "Call", method: "delete", args: { where: { slug: "call-one" } } },
|
|
589
|
+
expected: {
|
|
590
|
+
kind: "rewrite",
|
|
591
|
+
call: { model: "Call", method: "delete", args: { where: { slug: "call-one", AND: [predCall] } } }
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
name: "upsert-inject",
|
|
596
|
+
description: "upsert: predicate guards the update arm (probe honors it), create arm is force-filled",
|
|
597
|
+
scope: A,
|
|
598
|
+
call: {
|
|
599
|
+
model: "Call",
|
|
600
|
+
method: "upsert",
|
|
601
|
+
args: {
|
|
602
|
+
where: { slug: "call-one" },
|
|
603
|
+
create: { id: "c9", slug: "call-one", title: "t", secret: "x" },
|
|
604
|
+
update: { title: "u" }
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
expected: {
|
|
608
|
+
kind: "rewrite",
|
|
609
|
+
call: {
|
|
610
|
+
model: "Call",
|
|
611
|
+
method: "upsert",
|
|
612
|
+
args: {
|
|
613
|
+
where: { slug: "call-one", AND: [predCall] },
|
|
614
|
+
create: { id: "c9", slug: "call-one", title: "t", secret: "x", workspaceId: A },
|
|
615
|
+
update: { title: "u" }
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
name: "deletemany-none-pass",
|
|
622
|
+
description: 'deleteMany on a "none" model passes through',
|
|
623
|
+
scope: A,
|
|
624
|
+
call: { model: "Currency", method: "deleteMany", args: { where: { name: "Euro" } } },
|
|
625
|
+
expected: { kind: "passthrough" }
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
name: "null-scope-refuse",
|
|
629
|
+
description: "scope: null \u2014 every call on a scoped model refuses",
|
|
630
|
+
scope: null,
|
|
631
|
+
call: { model: "Call", method: "findMany" },
|
|
632
|
+
expected: { kind: "refuse", refusal: { model: "Call", path: "call.findMany", reason: "null_scope" } }
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
name: "null-scope-none-pass",
|
|
636
|
+
description: 'scope: null \u2014 "none" models still work',
|
|
637
|
+
scope: null,
|
|
638
|
+
call: { model: "Currency", method: "findMany" },
|
|
639
|
+
expected: { kind: "passthrough" }
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
name: "toone-implicit-refuse",
|
|
643
|
+
description: "an implicit-target nested verb (to-one update without a where) refuses off the tenancy edge",
|
|
644
|
+
scope: A,
|
|
645
|
+
call: {
|
|
646
|
+
model: "Currency",
|
|
647
|
+
method: "update",
|
|
648
|
+
args: { where: { code: "USD" }, data: { featuredCall: { update: { title: "x" } } } }
|
|
649
|
+
},
|
|
650
|
+
expected: {
|
|
651
|
+
kind: "refuse",
|
|
652
|
+
refusal: { model: "Call", path: "currency.update.data.featuredCall.update", reason: "nested_refused" }
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
name: "toone-disconnect-owner-pass",
|
|
657
|
+
description: "to-one disconnect on the FK-owning side passes \u2014 it writes only this row's own FK",
|
|
658
|
+
scope: A,
|
|
659
|
+
call: {
|
|
660
|
+
model: "Currency",
|
|
661
|
+
method: "update",
|
|
662
|
+
args: { where: { code: "USD" }, data: { featuredCall: { disconnect: true } } }
|
|
663
|
+
},
|
|
664
|
+
expected: { kind: "passthrough" }
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
name: "m2m-set-refuse",
|
|
668
|
+
description: "M2M set refuses on a scoped target \u2014 its clearing phase would silently unlink foreign rows",
|
|
669
|
+
scope: A,
|
|
670
|
+
call: { model: "Tag", method: "update", args: { where: { id: "t1" }, data: { calls: { set: [{ id: "c1" }] } } } },
|
|
671
|
+
expected: {
|
|
672
|
+
kind: "refuse",
|
|
673
|
+
refusal: { model: "Call", path: "tag.update.data.calls.set", reason: "nested_refused" }
|
|
674
|
+
}
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
name: "m2m-connect-inject",
|
|
678
|
+
description: "M2M connect is tenant-verified via the injected unique where",
|
|
679
|
+
scope: A,
|
|
680
|
+
call: {
|
|
681
|
+
model: "Tag",
|
|
682
|
+
method: "update",
|
|
683
|
+
args: { where: { id: "t1" }, data: { calls: { connect: [{ id: "c1" }] } } }
|
|
684
|
+
},
|
|
685
|
+
expected: {
|
|
686
|
+
kind: "rewrite",
|
|
687
|
+
call: {
|
|
688
|
+
model: "Tag",
|
|
689
|
+
method: "update",
|
|
690
|
+
args: { where: { id: "t1" }, data: { calls: { connect: [{ id: "c1", AND: [predCall] }] } } }
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
name: "nullable-column-equality",
|
|
696
|
+
description: "a nullable tenant column scopes by plain equality \u2014 NULL rows are invisible structurally",
|
|
697
|
+
scope: A,
|
|
698
|
+
call: { model: "Draft", method: "findMany" },
|
|
699
|
+
expected: {
|
|
700
|
+
kind: "rewrite",
|
|
701
|
+
call: { model: "Draft", method: "findMany", args: { where: { workspaceId: A } } }
|
|
702
|
+
}
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
name: "unknown-arg-preserved",
|
|
706
|
+
description: "unknown argument keys are preserved for the builder's own loud refusal (never silently dropped)",
|
|
707
|
+
scope: A,
|
|
708
|
+
call: { model: "Call", method: "findMany", args: { frobnicate: true } },
|
|
709
|
+
expected: {
|
|
710
|
+
kind: "rewrite",
|
|
711
|
+
call: { model: "Call", method: "findMany", args: { frobnicate: true, where: predCall } }
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
];
|
|
715
|
+
var BEHAVIOR_CHECKLIST = [
|
|
716
|
+
{ id: "foreign-findunique-null", description: "findUnique on tenant B's row (c2) returns null \u2014 indistinguishable from missing" },
|
|
717
|
+
{ id: "foreign-update-not-found", description: "update/delete addressing c2 fails exactly like a missing row (the binding's not-found error), and the row is untouched" },
|
|
718
|
+
{ id: "create-autofill-persisted", description: "create without the tenant column persists it with the scope value" },
|
|
719
|
+
{ id: "relation-filter-oracle-empty", description: "tag.findMany({ where: { calls: { some: { secret: <tenant B's secret> } } } }) returns [] \u2014 no cross-tenant existence oracle" },
|
|
720
|
+
{ id: "include-nesting-invisible", description: "include legs under a call returns only in-tenant rows; currency include featuredCall of a B-featured currency returns null" },
|
|
721
|
+
{ id: "aggregate-groupby-scoped", description: "count/aggregate/groupBy reflect only tenant A's rows" },
|
|
722
|
+
{ id: "upsert-both-arms", description: "upsert hits the update arm on an in-tenant row and the create arm (force-filled) on a miss" },
|
|
723
|
+
{ id: "upsert-foreign-collision-loud", description: "upsert whose unique key collides with tenant B's row takes the create arm and fails LOUDLY with a unique violation \u2014 never updates the foreign row" },
|
|
724
|
+
{ id: "tx-inheritance", description: "$transaction (callback AND array form) inherits the scope on every operation" },
|
|
725
|
+
{ id: "null-scope-refusal", description: 'a scope: null handle refuses scoped-model calls and still serves "none" models' },
|
|
726
|
+
{ id: "null-column-invisibility", description: "the NULL-tenant draft (d0) is invisible to scoped handles of BOTH tenants and reachable via the base client" },
|
|
727
|
+
{ id: "raw-refusal", description: "$queryRaw/$executeRaw (and unsafe variants) reject on the scoped handle with the typed scoped error" },
|
|
728
|
+
{ id: "unknown-arg-refusal", description: "an unknown argument key still fails loudly through the scoped handle" },
|
|
729
|
+
{ id: "updatemany-count-scoped", description: "updateMany matching rows in both tenants reports a count covering ONLY tenant A's rows" },
|
|
730
|
+
{ id: "nested-connect-cross-tenant-missing", description: "nested connect to tenant B's row (l3) fails exactly like connecting a nonexistent row, atomically (parent create rolls back)" },
|
|
731
|
+
{ id: "m2m-connect-cross-tenant-missing", description: "M2M connect to tenant B's call fails like a missing row" },
|
|
732
|
+
{ id: "nested-refuse-mode", description: 'with nested: "refuse" on Leg, nested connect refuses with the typed scoped error' },
|
|
733
|
+
{ id: "cursor-scoped", description: "cursor pagination returns only in-tenant rows; a foreign cursor id leaks nothing beyond the caller-supplied value" },
|
|
734
|
+
{ id: "scope-isolation-write-proof", description: "after a full scoped-handle session under tenant A, every row of tenant B is byte-identical to its seed state" }
|
|
735
|
+
];
|
|
736
|
+
|
|
737
|
+
// packages/scoped-conformance/src/index.ts
|
|
738
|
+
function describeRefusal(error) {
|
|
739
|
+
if (typeof error !== "object" || error === null)
|
|
740
|
+
return null;
|
|
741
|
+
const candidate = error;
|
|
742
|
+
if (candidate.name !== "ScopedQueryError")
|
|
743
|
+
return null;
|
|
744
|
+
if (typeof candidate.model !== "string" || typeof candidate.path !== "string" || typeof candidate.reason !== "string") {
|
|
745
|
+
return null;
|
|
746
|
+
}
|
|
747
|
+
return { model: candidate.model, path: candidate.path, reason: candidate.reason };
|
|
748
|
+
}
|
|
749
|
+
function refusalMatches(params) {
|
|
750
|
+
const actual = describeRefusal(params.error);
|
|
751
|
+
return actual !== null && actual.model === params.expected.model && actual.path === params.expected.path && actual.reason === params.expected.reason;
|
|
752
|
+
}
|
|
753
|
+
export {
|
|
754
|
+
refusalMatches,
|
|
755
|
+
describeRefusal,
|
|
756
|
+
TENANTS,
|
|
757
|
+
SCOPED_SEED,
|
|
758
|
+
SCOPED_MODELS_LEG_REFUSE,
|
|
759
|
+
SCOPED_MODELS,
|
|
760
|
+
SCOPED_FIXTURE_PRISMA,
|
|
761
|
+
SCOPED_FIXTURE_IR,
|
|
762
|
+
REWRITE_VECTORS,
|
|
763
|
+
BEHAVIOR_CHECKLIST
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
//# debugId=D0D7C59A47D0228364756E2164756E21
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/fixture.ts", "../src/vectors.ts", "../src/index.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * The canonical conformance fixture: one small schema exercising every\n * tenancy shape the $scoped contract names —\n * Call { column: \"workspaceId\" } tenant column on the row\n * Leg { through: \"call\" } tenant lives on the to-one parent\n * Currency \"none\" global reference data (with a\n * nullable to-one INTO a scoped model)\n * Draft { column: \"workspaceId\" } NULLABLE tenant column (NULL rows\n * are invisible to every scoped handle)\n * Tag \"none\" implicit M2M with a scoped model\n *\n * Shipped in two equivalent forms: Prisma schema text (for bindings that\n * parse) and a neutral structural IR (for bindings that build metadata\n * directly). This package has NO dependencies — the IR types below are\n * structural mirrors, not imports.\n */\n\n// ─── Neutral schema IR (structural) ───────────────────────────────\n\nexport type FixtureField = {\n readonly name: string;\n readonly type: string | { readonly enum: string };\n readonly isList: boolean;\n readonly isOptional: boolean;\n readonly isId: boolean;\n readonly isUnique: boolean;\n readonly isUpdatedAt: boolean;\n readonly default?: { readonly kind: string; readonly value?: unknown };\n};\n\nexport type FixtureRelation = {\n readonly name: string;\n readonly model: string;\n readonly targetModel: string;\n readonly type: \"oneToOne\" | \"oneToMany\" | \"manyToOne\" | \"manyToMany\";\n readonly relationName?: string;\n readonly fields?: readonly string[];\n readonly references?: readonly string[];\n readonly isList: boolean;\n readonly isOptional: boolean;\n};\n\nexport type FixtureModel = {\n readonly name: string;\n readonly fields: readonly FixtureField[];\n readonly relations: readonly FixtureRelation[];\n readonly indexes: readonly unknown[];\n readonly isView: boolean;\n};\n\nexport type FixtureSchema = {\n readonly datasource: { readonly provider: string };\n readonly enums: readonly unknown[];\n readonly extensions: readonly unknown[];\n readonly models: readonly FixtureModel[];\n};\n\nfunction scalar(params: {\n name: string;\n type: string;\n optional?: boolean;\n id?: boolean;\n unique?: boolean;\n}): FixtureField {\n return {\n name: params.name,\n type: params.type,\n isList: false,\n isOptional: params.optional === true,\n isId: params.id === true,\n isUnique: params.unique === true,\n isUpdatedAt: false,\n };\n}\n\n/** The fixture as Prisma schema text (datasource provider is up to the harness). */\nexport const SCOPED_FIXTURE_PRISMA: string = `\ndatasource db {\n provider = \"postgresql\"\n url = env(\"DATABASE_URL\")\n}\n\nmodel Call {\n id String @id\n workspaceId String\n slug String @unique\n title String\n secret String\n legs Leg[]\n featured Currency[]\n tags Tag[]\n}\n\nmodel Leg {\n id String @id\n callId String\n call Call @relation(fields: [callId], references: [id])\n duration Int\n}\n\nmodel Currency {\n code String @id\n name String\n featuredCallId String?\n featuredCall Call? @relation(fields: [featuredCallId], references: [id])\n}\n\nmodel Draft {\n id String @id\n workspaceId String?\n title String\n}\n\nmodel Tag {\n id String @id\n name String @unique\n calls Call[]\n}\n`;\n\n/** The same fixture as neutral structural IR. */\nexport const SCOPED_FIXTURE_IR: FixtureSchema = {\n datasource: { provider: \"postgresql\" },\n enums: [],\n extensions: [],\n models: [\n {\n name: \"Call\",\n fields: [\n scalar({ name: \"id\", type: \"String\", id: true }),\n scalar({ name: \"workspaceId\", type: \"String\" }),\n scalar({ name: \"slug\", type: \"String\", unique: true }),\n scalar({ name: \"title\", type: \"String\" }),\n scalar({ name: \"secret\", type: \"String\" }),\n ],\n relations: [\n { name: \"legs\", model: \"Call\", targetModel: \"Leg\", type: \"oneToMany\", isList: true, isOptional: false },\n { name: \"featured\", model: \"Call\", targetModel: \"Currency\", type: \"oneToMany\", isList: true, isOptional: false },\n { name: \"tags\", model: \"Call\", targetModel: \"Tag\", type: \"manyToMany\", isList: true, isOptional: false },\n ],\n indexes: [],\n isView: false,\n },\n {\n name: \"Leg\",\n fields: [\n scalar({ name: \"id\", type: \"String\", id: true }),\n scalar({ name: \"callId\", type: \"String\" }),\n scalar({ name: \"duration\", type: \"Int\" }),\n ],\n relations: [\n {\n name: \"call\",\n model: \"Leg\",\n targetModel: \"Call\",\n type: \"manyToOne\",\n fields: [\"callId\"],\n references: [\"id\"],\n isList: false,\n isOptional: false,\n },\n ],\n indexes: [],\n isView: false,\n },\n {\n name: \"Currency\",\n fields: [\n scalar({ name: \"code\", type: \"String\", id: true }),\n scalar({ name: \"name\", type: \"String\" }),\n scalar({ name: \"featuredCallId\", type: \"String\", optional: true }),\n ],\n relations: [\n {\n name: \"featuredCall\",\n model: \"Currency\",\n targetModel: \"Call\",\n type: \"manyToOne\",\n fields: [\"featuredCallId\"],\n references: [\"id\"],\n isList: false,\n isOptional: true,\n },\n ],\n indexes: [],\n isView: false,\n },\n {\n name: \"Draft\",\n fields: [\n scalar({ name: \"id\", type: \"String\", id: true }),\n scalar({ name: \"workspaceId\", type: \"String\", optional: true }),\n scalar({ name: \"title\", type: \"String\" }),\n ],\n relations: [],\n indexes: [],\n isView: false,\n },\n {\n name: \"Tag\",\n fields: [scalar({ name: \"id\", type: \"String\", id: true }), scalar({ name: \"name\", type: \"String\", unique: true })],\n relations: [\n { name: \"calls\", model: \"Tag\", targetModel: \"Call\", type: \"manyToMany\", isList: true, isOptional: false },\n ],\n indexes: [],\n isView: false,\n },\n ],\n};\n\n// ─── The canonical tenancy map ────────────────────────────────────\n\nexport type ConformanceNestedMode = \"verify\" | \"refuse\";\n\nexport type ConformanceModelEntry =\n | \"none\"\n | { readonly column: string; readonly nested?: ConformanceNestedMode }\n | { readonly through: string; readonly nested?: ConformanceNestedMode };\n\nexport type ConformanceModelsMap = Readonly<Record<string, ConformanceModelEntry>>;\n\n/** The default tenancy map every vector assumes unless it carries its own. */\nexport const SCOPED_MODELS: ConformanceModelsMap = {\n Call: { column: \"workspaceId\" },\n Leg: { through: \"call\" },\n Currency: \"none\",\n Draft: { column: \"workspaceId\" },\n Tag: \"none\",\n};\n\n/** Variant map with strict nested mode on Leg (the refuse-mode vectors use it). */\nexport const SCOPED_MODELS_LEG_REFUSE: ConformanceModelsMap = {\n ...SCOPED_MODELS,\n Leg: { through: \"call\", nested: \"refuse\" },\n};\n\n// ─── Seed ─────────────────────────────────────────────────────────\n\n/** The two tenants every behavior case runs against. */\nexport const TENANTS: { readonly a: string; readonly b: string } = { a: \"ws_1\", b: \"ws_2\" };\n\nexport type SeedSpec = {\n readonly calls: ReadonlyArray<{\n readonly id: string;\n readonly workspaceId: string;\n readonly slug: string;\n readonly title: string;\n readonly secret: string;\n }>;\n readonly legs: ReadonlyArray<{ readonly id: string; readonly callId: string; readonly duration: number }>;\n readonly currencies: ReadonlyArray<{\n readonly code: string;\n readonly name: string;\n readonly featuredCallId: string | null;\n }>;\n readonly drafts: ReadonlyArray<{ readonly id: string; readonly workspaceId: string | null; readonly title: string }>;\n readonly tags: ReadonlyArray<{ readonly id: string; readonly name: string; readonly callIds: readonly string[] }>;\n};\n\n/**\n * Canonical seed: tenant A owns c1 (legs l1, l2), tenant B owns c2 (leg l3);\n * USD features tenant B's call (the cross-tenant to-one every oracle vector\n * probes), NOK features tenant A's, EUR features none; d0 has a NULL tenant\n * (invisible to every scoped handle); tag t1 links BOTH tenants' calls.\n */\nexport const SCOPED_SEED: SeedSpec = {\n calls: [\n { id: \"c1\", workspaceId: TENANTS.a, slug: \"call-one\", title: \"standup\", secret: \"alpha-secret\" },\n { id: \"c2\", workspaceId: TENANTS.b, slug: \"call-two\", title: \"standup\", secret: \"beta-secret\" },\n ],\n legs: [\n { id: \"l1\", callId: \"c1\", duration: 60 },\n { id: \"l2\", callId: \"c1\", duration: 120 },\n { id: \"l3\", callId: \"c2\", duration: 240 },\n ],\n currencies: [\n { code: \"USD\", name: \"US Dollar\", featuredCallId: \"c2\" },\n { code: \"NOK\", name: \"Norwegian Krone\", featuredCallId: \"c1\" },\n { code: \"EUR\", name: \"Euro\", featuredCallId: null },\n ],\n drafts: [\n { id: \"d1\", workspaceId: TENANTS.a, title: \"mine\" },\n { id: \"d2\", workspaceId: TENANTS.b, title: \"theirs\" },\n { id: \"d0\", workspaceId: null, title: \"unassigned\" },\n ],\n tags: [{ id: \"t1\", name: \"shared\", callIds: [\"c1\", \"c2\"] }],\n};\n",
|
|
6
|
+
"/**\n * The conformance vectors.\n *\n * REWRITE tier (pure, no database): `{ models, scope, call }` must produce\n * exactly `expected` — a rewritten call, a byte-identical passthrough, or a\n * refusal matched on `{ model, path, reason }` (never on message text). Both\n * bindings run every vector through their own pure rewriter.\n *\n * BEHAVIOR tier (live database): a checklist of named, normative scenarios\n * each binding implements as tests against the canonical fixture + seed\n * (`SCOPED_FIXTURE_*`, `SCOPED_SEED`). The ids are the shared contract; a\n * binding's suite must cover every id.\n */\n\nimport { SCOPED_MODELS, SCOPED_MODELS_LEG_REFUSE, TENANTS } from \"./fixture.ts\";\nimport type { ConformanceModelsMap } from \"./fixture.ts\";\n\n// ─── Types ────────────────────────────────────────────────────────\n\n/** One ORM call in the shared `{ model, method, args }` language. */\nexport type ScopedCall = {\n readonly model: string;\n readonly method: string;\n readonly args?: Record<string, unknown>;\n};\n\nexport type ExpectedRefusal = {\n readonly model: string;\n readonly path: string;\n readonly reason: string;\n};\n\nexport type RewriteExpectation =\n | { readonly kind: \"rewrite\"; readonly call: ScopedCall }\n | { readonly kind: \"passthrough\" }\n | { readonly kind: \"refuse\"; readonly refusal: ExpectedRefusal };\n\nexport type RewriteVector = {\n readonly name: string;\n readonly description: string;\n /** Tenancy map for this vector; defaults to `SCOPED_MODELS`. */\n readonly models?: ConformanceModelsMap;\n readonly scope: string | null;\n readonly call: ScopedCall;\n readonly expected: RewriteExpectation;\n};\n\nexport type BehaviorCase = {\n readonly id: string;\n readonly description: string;\n};\n\n// ─── Shared predicate spellings ───────────────────────────────────\n\nconst A: string = TENANTS.a;\nconst B: string = TENANTS.b;\nconst predCall = { workspaceId: A } as const;\nconst predLeg = { call: { workspaceId: A } } as const;\n\nconst CALL_CREATE_DATA = { id: \"c9\", slug: \"call-nine\", title: \"retro\", secret: \"s9\" } as const;\n\n// ─── Rewrite vectors ──────────────────────────────────────────────\n\nexport const REWRITE_VECTORS: readonly RewriteVector[] = [\n {\n name: \"find-merge\",\n description: \"findMany AND-merges the tenant predicate into the user's where\",\n scope: A,\n call: { model: \"Call\", method: \"findMany\", args: { where: { title: \"standup\" } } },\n expected: {\n kind: \"rewrite\",\n call: { model: \"Call\", method: \"findMany\", args: { where: { AND: [{ title: \"standup\" }, predCall] } } },\n },\n },\n {\n name: \"find-no-where\",\n description: \"findMany without a where gets the bare predicate\",\n scope: A,\n call: { model: \"Call\", method: \"findMany\" },\n expected: { kind: \"rewrite\", call: { model: \"Call\", method: \"findMany\", args: { where: predCall } } },\n },\n {\n name: \"none-passthrough\",\n description: '\"none\" models pass through untouched',\n scope: A,\n call: { model: \"Currency\", method: \"findMany\", args: { where: { name: \"Euro\" } } },\n expected: { kind: \"passthrough\" },\n },\n {\n name: \"findunique-rewrite\",\n description: \"findUnique rewrites to findFirst with the unique key + predicate; a foreign row reads as null\",\n scope: A,\n call: { model: \"Call\", method: \"findUnique\", args: { where: { id: \"c1\" } } },\n expected: {\n kind: \"rewrite\",\n call: { model: \"Call\", method: \"findFirst\", args: { where: { AND: [{ id: \"c1\" }, predCall] } } },\n },\n },\n {\n name: \"findunique-none-kept\",\n description: 'findUnique on a \"none\" model stays findUnique',\n scope: A,\n call: { model: \"Currency\", method: \"findUnique\", args: { where: { code: \"USD\" } } },\n expected: { kind: \"passthrough\" },\n },\n {\n name: \"through-find\",\n description: \"a { through } model filters via its tenancy parent\",\n scope: A,\n call: { model: \"Leg\", method: \"findMany\" },\n expected: { kind: \"rewrite\", call: { model: \"Leg\", method: \"findMany\", args: { where: predLeg } } },\n },\n {\n name: \"count-merge\",\n description: \"count AND-merges the predicate\",\n scope: A,\n call: { model: \"Call\", method: \"count\", args: { where: { title: \"standup\" } } },\n expected: {\n kind: \"rewrite\",\n call: { model: \"Call\", method: \"count\", args: { where: { AND: [{ title: \"standup\" }, predCall] } } },\n },\n },\n {\n name: \"aggregate-merge\",\n description: \"aggregate gets the predicate\",\n scope: A,\n call: { model: \"Call\", method: \"aggregate\", args: { _count: true } },\n expected: {\n kind: \"rewrite\",\n call: { model: \"Call\", method: \"aggregate\", args: { _count: true, where: predCall } },\n },\n },\n {\n name: \"groupby-merge\",\n description: \"groupBy gets the predicate\",\n scope: A,\n call: { model: \"Call\", method: \"groupBy\", args: { by: [\"title\"], _count: true } },\n expected: {\n kind: \"rewrite\",\n call: { model: \"Call\", method: \"groupBy\", args: { by: [\"title\"], _count: true, where: predCall } },\n },\n },\n {\n name: \"relation-is-inject\",\n description: \"a to-one relation filter into a scoped model gets the predicate (bare object = is)\",\n scope: A,\n call: { model: \"Currency\", method: \"findMany\", args: { where: { featuredCall: { title: \"standup\" } } } },\n expected: {\n kind: \"rewrite\",\n call: {\n model: \"Currency\",\n method: \"findMany\",\n args: { where: { featuredCall: { is: { AND: [{ title: \"standup\" }, predCall] } } } },\n },\n },\n },\n {\n name: \"relation-is-null-flip\",\n description: \"is: null means 'no VISIBLE parent' — a foreign parent reads as absent\",\n scope: A,\n call: { model: \"Currency\", method: \"findMany\", args: { where: { featuredCall: null } } },\n expected: {\n kind: \"rewrite\",\n call: { model: \"Currency\", method: \"findMany\", args: { where: { featuredCall: { isNot: predCall } } } },\n },\n },\n {\n name: \"relation-isnot-null-flip\",\n description: \"isNot: null means 'a VISIBLE parent exists'\",\n scope: A,\n call: { model: \"Currency\", method: \"findMany\", args: { where: { featuredCall: { isNot: null } } } },\n expected: {\n kind: \"rewrite\",\n call: { model: \"Currency\", method: \"findMany\", args: { where: { featuredCall: { is: predCall } } } },\n },\n },\n {\n name: \"m2m-some-inject\",\n description: \"a to-many (implicit M2M) filter into a scoped model gets the predicate — the cross-tenant existence oracle closes\",\n scope: A,\n call: { model: \"Tag\", method: \"findMany\", args: { where: { calls: { some: { secret: \"beta-secret\" } } } } },\n expected: {\n kind: \"rewrite\",\n call: {\n model: \"Tag\",\n method: \"findMany\",\n args: { where: { calls: { some: { AND: [{ secret: \"beta-secret\" }, predCall] } } } },\n },\n },\n },\n {\n name: \"every-rewrite\",\n description: \"every p becomes none(tenant AND NOT p): only VISIBLE children must satisfy p\",\n scope: A,\n call: { model: \"Call\", method: \"findMany\", args: { where: { legs: { every: { duration: { gt: 100 } } } } } },\n expected: {\n kind: \"rewrite\",\n call: {\n model: \"Call\",\n method: \"findMany\",\n args: {\n where: {\n AND: [{ legs: { none: { AND: [predLeg, { NOT: [{ duration: { gt: 100 } }] }] } } }, predCall],\n },\n },\n },\n },\n },\n {\n name: \"orderby-refuse\",\n description: \"orderBy through a relation onto a scoped model refuses (ordering would leak foreign values)\",\n scope: A,\n call: { model: \"Currency\", method: \"findMany\", args: { orderBy: { featuredCall: { title: \"asc\" } } } },\n expected: {\n kind: \"refuse\",\n refusal: { model: \"Call\", path: \"currency.findMany.orderBy.featuredCall\", reason: \"relation_orderby_on_scoped\" },\n },\n },\n {\n name: \"orderby-tenancy-edge\",\n description: \"a through model may order by its OWN tenancy parent (in-tenant by definition)\",\n scope: A,\n call: { model: \"Leg\", method: \"findMany\", args: { orderBy: { call: { title: \"asc\" } } } },\n expected: {\n kind: \"rewrite\",\n call: { model: \"Leg\", method: \"findMany\", args: { orderBy: { call: { title: \"asc\" } }, where: predLeg } },\n },\n },\n {\n name: \"include-inject\",\n description: \"include of a scoped child gets the predicate in its relation args\",\n scope: A,\n call: { model: \"Call\", method: \"findMany\", args: { include: { legs: true } } },\n expected: {\n kind: \"rewrite\",\n call: { model: \"Call\", method: \"findMany\", args: { include: { legs: { where: predLeg } }, where: predCall } },\n },\n },\n {\n name: \"include-child-where\",\n description: \"a child where AND-merges with the predicate\",\n scope: A,\n call: { model: \"Call\", method: \"findMany\", args: { include: { legs: { where: { duration: { gt: 60 } } } } } },\n expected: {\n kind: \"rewrite\",\n call: {\n model: \"Call\",\n method: \"findMany\",\n args: { include: { legs: { where: { AND: [{ duration: { gt: 60 } }, predLeg] } } }, where: predCall },\n },\n },\n },\n {\n name: \"include-none-pass\",\n description: 'including a \"none\" child passes through',\n scope: A,\n call: { model: \"Call\", method: \"findMany\", args: { include: { featured: true } } },\n expected: {\n kind: \"rewrite\",\n call: { model: \"Call\", method: \"findMany\", args: { include: { featured: true }, where: predCall } },\n },\n },\n {\n name: \"count-relation-refuse\",\n description: \"_count of a scoped relation refuses (relation counts take no filter — it would count foreign rows)\",\n scope: A,\n call: { model: \"Tag\", method: \"findMany\", args: { include: { _count: { select: { calls: true } } } } },\n expected: {\n kind: \"refuse\",\n refusal: { model: \"Call\", path: \"tag.findMany.include._count.calls\", reason: \"count_on_scoped_relation\" },\n },\n },\n {\n name: \"count-tenancy-edge-ok\",\n description: \"_count over the tenancy inverse is allowed — those children ARE this row's tenant\",\n scope: A,\n call: { model: \"Call\", method: \"findMany\", args: { include: { _count: { select: { legs: true } } } } },\n expected: {\n kind: \"rewrite\",\n call: {\n model: \"Call\",\n method: \"findMany\",\n args: { include: { _count: { select: { legs: true } } }, where: predCall },\n },\n },\n },\n {\n name: \"create-autofill\",\n description: \"create fills the tenant column\",\n scope: A,\n call: { model: \"Call\", method: \"create\", args: { data: { ...CALL_CREATE_DATA } } },\n expected: {\n kind: \"rewrite\",\n call: { model: \"Call\", method: \"create\", args: { data: { ...CALL_CREATE_DATA, workspaceId: A } } },\n },\n },\n {\n name: \"create-mismatch-refuse\",\n description: \"an explicit DIFFERENT tenant value refuses before any SQL\",\n scope: A,\n call: { model: \"Call\", method: \"create\", args: { data: { ...CALL_CREATE_DATA, workspaceId: B } } },\n expected: {\n kind: \"refuse\",\n refusal: { model: \"Call\", path: \"call.create.data.workspaceId\", reason: \"explicit_scope_mismatch\" },\n },\n },\n {\n name: \"create-null-refuse\",\n description: \"an explicit NULL tenant value refuses (NULL rows are reachable only via the base client)\",\n scope: A,\n call: { model: \"Call\", method: \"create\", args: { data: { ...CALL_CREATE_DATA, workspaceId: null } } },\n expected: {\n kind: \"refuse\",\n refusal: { model: \"Call\", path: \"call.create.data.workspaceId\", reason: \"explicit_scope_mismatch\" },\n },\n },\n {\n name: \"createmany-autofill\",\n description: \"createMany fills every row\",\n scope: A,\n call: {\n model: \"Draft\",\n method: \"createMany\",\n args: { data: [{ id: \"d8\", title: \"a\" }, { id: \"d9\", title: \"b\" }] },\n },\n expected: {\n kind: \"rewrite\",\n call: {\n model: \"Draft\",\n method: \"createMany\",\n args: { data: [{ id: \"d8\", title: \"a\", workspaceId: A }, { id: \"d9\", title: \"b\", workspaceId: A }] },\n },\n },\n },\n {\n name: \"nested-create-tenancy-edge\",\n description: \"nested create under the tenancy inverse passes structurally (the parent defines the child's tenant)\",\n scope: A,\n call: {\n model: \"Call\",\n method: \"create\",\n args: { data: { ...CALL_CREATE_DATA, legs: { create: [{ id: \"l9\", duration: 30 }] } } },\n },\n expected: {\n kind: \"rewrite\",\n call: {\n model: \"Call\",\n method: \"create\",\n args: {\n data: { ...CALL_CREATE_DATA, workspaceId: A, legs: { create: [{ id: \"l9\", duration: 30 }] } },\n },\n },\n },\n },\n {\n name: \"nested-connect-inject\",\n description: \"nested connect becomes a tenant-verified lookup (predicate joins the unique where)\",\n scope: A,\n call: {\n model: \"Call\",\n method: \"create\",\n args: { data: { ...CALL_CREATE_DATA, legs: { connect: [{ id: \"l1\" }] } } },\n },\n expected: {\n kind: \"rewrite\",\n call: {\n model: \"Call\",\n method: \"create\",\n args: {\n data: { ...CALL_CREATE_DATA, workspaceId: A, legs: { connect: [{ id: \"l1\", AND: [predLeg] }] } },\n },\n },\n },\n },\n {\n name: \"nested-connect-refuse-mode\",\n description: 'nested: \"refuse\" rejects reference verbs on that model',\n models: SCOPED_MODELS_LEG_REFUSE,\n scope: A,\n call: {\n model: \"Call\",\n method: \"create\",\n args: { data: { ...CALL_CREATE_DATA, legs: { connect: [{ id: \"l1\" }] } } },\n },\n expected: {\n kind: \"refuse\",\n refusal: { model: \"Leg\", path: \"call.create.data.legs.connect\", reason: \"nested_refused\" },\n },\n },\n {\n name: \"through-create-missing-parent\",\n description: \"creating a through-scoped row without its tenancy parent refuses\",\n scope: A,\n call: { model: \"Leg\", method: \"create\", args: { data: { id: \"l9\", duration: 5 } } },\n expected: { kind: \"refuse\", refusal: { model: \"Leg\", path: \"leg.create.data\", reason: \"through_fk_write\" } },\n },\n {\n name: \"through-create-bare-fk\",\n description: \"a bare foreign-key write on a through-scoped model refuses — the relation verbs are the verified path\",\n scope: A,\n call: { model: \"Leg\", method: \"create\", args: { data: { id: \"l9\", duration: 5, callId: \"c1\" } } },\n expected: {\n kind: \"refuse\",\n refusal: { model: \"Leg\", path: \"leg.create.data.callId\", reason: \"through_fk_write\" },\n },\n },\n {\n name: \"through-create-connect\",\n description: \"creating a through-scoped row via connect verifies the parent in-tenant\",\n scope: A,\n call: {\n model: \"Leg\",\n method: \"create\",\n args: { data: { id: \"l9\", duration: 5, call: { connect: { slug: \"call-one\" } } } },\n },\n expected: {\n kind: \"rewrite\",\n call: {\n model: \"Leg\",\n method: \"create\",\n args: { data: { id: \"l9\", duration: 5, call: { connect: { slug: \"call-one\", AND: [predCall] } } } },\n },\n },\n },\n {\n name: \"update-inject\",\n description: \"update injects the predicate into the unique where — compare-and-write, foreign row behaves like missing\",\n scope: A,\n call: { model: \"Call\", method: \"update\", args: { where: { id: \"c1\" }, data: { title: \"renamed\" } } },\n expected: {\n kind: \"rewrite\",\n call: {\n model: \"Call\",\n method: \"update\",\n args: { where: { id: \"c1\", AND: [predCall] }, data: { title: \"renamed\" } },\n },\n },\n },\n {\n name: \"update-column-guard\",\n description: \"update writing the tenant column refuses — a scoped handle cannot move rows between tenants\",\n scope: A,\n call: { model: \"Call\", method: \"update\", args: { where: { id: \"c1\" }, data: { workspaceId: B } } },\n expected: {\n kind: \"refuse\",\n refusal: { model: \"Call\", path: \"call.update.data.workspaceId\", reason: \"explicit_scope_mismatch\" },\n },\n },\n {\n name: \"updatemany-merge\",\n description: \"updateMany AND-merges; the count reflects only in-tenant rows\",\n scope: A,\n call: { model: \"Call\", method: \"updateMany\", args: { where: { title: \"standup\" }, data: { secret: \"s\" } } },\n expected: {\n kind: \"rewrite\",\n call: {\n model: \"Call\",\n method: \"updateMany\",\n args: { where: { AND: [{ title: \"standup\" }, predCall] }, data: { secret: \"s\" } },\n },\n },\n },\n {\n name: \"delete-inject\",\n description: \"delete injects the predicate — a foreign row behaves like a missing one\",\n scope: A,\n call: { model: \"Call\", method: \"delete\", args: { where: { slug: \"call-one\" } } },\n expected: {\n kind: \"rewrite\",\n call: { model: \"Call\", method: \"delete\", args: { where: { slug: \"call-one\", AND: [predCall] } } },\n },\n },\n {\n name: \"upsert-inject\",\n description: \"upsert: predicate guards the update arm (probe honors it), create arm is force-filled\",\n scope: A,\n call: {\n model: \"Call\",\n method: \"upsert\",\n args: {\n where: { slug: \"call-one\" },\n create: { id: \"c9\", slug: \"call-one\", title: \"t\", secret: \"x\" },\n update: { title: \"u\" },\n },\n },\n expected: {\n kind: \"rewrite\",\n call: {\n model: \"Call\",\n method: \"upsert\",\n args: {\n where: { slug: \"call-one\", AND: [predCall] },\n create: { id: \"c9\", slug: \"call-one\", title: \"t\", secret: \"x\", workspaceId: A },\n update: { title: \"u\" },\n },\n },\n },\n },\n {\n name: \"deletemany-none-pass\",\n description: 'deleteMany on a \"none\" model passes through',\n scope: A,\n call: { model: \"Currency\", method: \"deleteMany\", args: { where: { name: \"Euro\" } } },\n expected: { kind: \"passthrough\" },\n },\n {\n name: \"null-scope-refuse\",\n description: \"scope: null — every call on a scoped model refuses\",\n scope: null,\n call: { model: \"Call\", method: \"findMany\" },\n expected: { kind: \"refuse\", refusal: { model: \"Call\", path: \"call.findMany\", reason: \"null_scope\" } },\n },\n {\n name: \"null-scope-none-pass\",\n description: 'scope: null — \"none\" models still work',\n scope: null,\n call: { model: \"Currency\", method: \"findMany\" },\n expected: { kind: \"passthrough\" },\n },\n {\n name: \"toone-implicit-refuse\",\n description: \"an implicit-target nested verb (to-one update without a where) refuses off the tenancy edge\",\n scope: A,\n call: {\n model: \"Currency\",\n method: \"update\",\n args: { where: { code: \"USD\" }, data: { featuredCall: { update: { title: \"x\" } } } },\n },\n expected: {\n kind: \"refuse\",\n refusal: { model: \"Call\", path: \"currency.update.data.featuredCall.update\", reason: \"nested_refused\" },\n },\n },\n {\n name: \"toone-disconnect-owner-pass\",\n description: \"to-one disconnect on the FK-owning side passes — it writes only this row's own FK\",\n scope: A,\n call: {\n model: \"Currency\",\n method: \"update\",\n args: { where: { code: \"USD\" }, data: { featuredCall: { disconnect: true } } },\n },\n expected: { kind: \"passthrough\" },\n },\n {\n name: \"m2m-set-refuse\",\n description: \"M2M set refuses on a scoped target — its clearing phase would silently unlink foreign rows\",\n scope: A,\n call: { model: \"Tag\", method: \"update\", args: { where: { id: \"t1\" }, data: { calls: { set: [{ id: \"c1\" }] } } } },\n expected: {\n kind: \"refuse\",\n refusal: { model: \"Call\", path: \"tag.update.data.calls.set\", reason: \"nested_refused\" },\n },\n },\n {\n name: \"m2m-connect-inject\",\n description: \"M2M connect is tenant-verified via the injected unique where\",\n scope: A,\n call: {\n model: \"Tag\",\n method: \"update\",\n args: { where: { id: \"t1\" }, data: { calls: { connect: [{ id: \"c1\" }] } } },\n },\n expected: {\n kind: \"rewrite\",\n call: {\n model: \"Tag\",\n method: \"update\",\n args: { where: { id: \"t1\" }, data: { calls: { connect: [{ id: \"c1\", AND: [predCall] }] } } },\n },\n },\n },\n {\n name: \"nullable-column-equality\",\n description: \"a nullable tenant column scopes by plain equality — NULL rows are invisible structurally\",\n scope: A,\n call: { model: \"Draft\", method: \"findMany\" },\n expected: {\n kind: \"rewrite\",\n call: { model: \"Draft\", method: \"findMany\", args: { where: { workspaceId: A } } },\n },\n },\n {\n name: \"unknown-arg-preserved\",\n description: \"unknown argument keys are preserved for the builder's own loud refusal (never silently dropped)\",\n scope: A,\n call: { model: \"Call\", method: \"findMany\", args: { frobnicate: true } },\n expected: {\n kind: \"rewrite\",\n call: { model: \"Call\", method: \"findMany\", args: { frobnicate: true, where: predCall } },\n },\n },\n];\n\n// ─── Behavior checklist ───────────────────────────────────────────\n\n/**\n * Live-database scenarios every binding's suite must implement against the\n * canonical fixture + seed, under tenant A unless stated. The id is the\n * contract; each binding's test names the id it covers.\n */\nexport const BEHAVIOR_CHECKLIST: readonly BehaviorCase[] = [\n { id: \"foreign-findunique-null\", description: \"findUnique on tenant B's row (c2) returns null — indistinguishable from missing\" },\n { id: \"foreign-update-not-found\", description: \"update/delete addressing c2 fails exactly like a missing row (the binding's not-found error), and the row is untouched\" },\n { id: \"create-autofill-persisted\", description: \"create without the tenant column persists it with the scope value\" },\n { id: \"relation-filter-oracle-empty\", description: \"tag.findMany({ where: { calls: { some: { secret: <tenant B's secret> } } } }) returns [] — no cross-tenant existence oracle\" },\n { id: \"include-nesting-invisible\", description: \"include legs under a call returns only in-tenant rows; currency include featuredCall of a B-featured currency returns null\" },\n { id: \"aggregate-groupby-scoped\", description: \"count/aggregate/groupBy reflect only tenant A's rows\" },\n { id: \"upsert-both-arms\", description: \"upsert hits the update arm on an in-tenant row and the create arm (force-filled) on a miss\" },\n { id: \"upsert-foreign-collision-loud\", description: \"upsert whose unique key collides with tenant B's row takes the create arm and fails LOUDLY with a unique violation — never updates the foreign row\" },\n { id: \"tx-inheritance\", description: \"$transaction (callback AND array form) inherits the scope on every operation\" },\n { id: \"null-scope-refusal\", description: \"a scope: null handle refuses scoped-model calls and still serves \\\"none\\\" models\" },\n { id: \"null-column-invisibility\", description: \"the NULL-tenant draft (d0) is invisible to scoped handles of BOTH tenants and reachable via the base client\" },\n { id: \"raw-refusal\", description: \"$queryRaw/$executeRaw (and unsafe variants) reject on the scoped handle with the typed scoped error\" },\n { id: \"unknown-arg-refusal\", description: \"an unknown argument key still fails loudly through the scoped handle\" },\n { id: \"updatemany-count-scoped\", description: \"updateMany matching rows in both tenants reports a count covering ONLY tenant A's rows\" },\n { id: \"nested-connect-cross-tenant-missing\", description: \"nested connect to tenant B's row (l3) fails exactly like connecting a nonexistent row, atomically (parent create rolls back)\" },\n { id: \"m2m-connect-cross-tenant-missing\", description: \"M2M connect to tenant B's call fails like a missing row\" },\n { id: \"nested-refuse-mode\", description: \"with nested: \\\"refuse\\\" on Leg, nested connect refuses with the typed scoped error\" },\n { id: \"cursor-scoped\", description: \"cursor pagination returns only in-tenant rows; a foreign cursor id leaks nothing beyond the caller-supplied value\" },\n { id: \"scope-isolation-write-proof\", description: \"after a full scoped-handle session under tenant A, every row of tenant B is byte-identical to its seed state\" },\n];\n",
|
|
7
|
+
"/**\n * @vibeorm/scoped-conformance — the binding-agnostic conformance artifact for\n * the tenant-scoped database handle (`$scoped` in VibeORM, the Prisma\n * client-extension binding in pekka/shared).\n *\n * Dependency-free by design: both bindings consume the SAME fixture, seed,\n * tenancy map and vectors, so the contract cannot drift between them.\n */\n\nexport {\n SCOPED_FIXTURE_IR,\n SCOPED_FIXTURE_PRISMA,\n SCOPED_MODELS,\n SCOPED_MODELS_LEG_REFUSE,\n SCOPED_SEED,\n TENANTS,\n} from \"./fixture.ts\";\nexport type {\n ConformanceModelEntry,\n ConformanceModelsMap,\n ConformanceNestedMode,\n FixtureField,\n FixtureModel,\n FixtureRelation,\n FixtureSchema,\n SeedSpec,\n} from \"./fixture.ts\";\n\nexport { BEHAVIOR_CHECKLIST, REWRITE_VECTORS } from \"./vectors.ts\";\nexport type {\n BehaviorCase,\n ExpectedRefusal,\n RewriteExpectation,\n RewriteVector,\n ScopedCall,\n} from \"./vectors.ts\";\n\n/**\n * Read the `{ model, path, reason }` triple off a binding's refusal error.\n * Matches structurally (never `instanceof` — each binding ships its own\n * `ScopedQueryError` class with these fields and `name: \"ScopedQueryError\"`).\n */\nexport function describeRefusal(error: unknown): { model: string; path: string; reason: string } | null {\n if (typeof error !== \"object\" || error === null) return null;\n const candidate = error as { name?: unknown; model?: unknown; path?: unknown; reason?: unknown };\n if (candidate.name !== \"ScopedQueryError\") return null;\n if (typeof candidate.model !== \"string\" || typeof candidate.path !== \"string\" || typeof candidate.reason !== \"string\") {\n return null;\n }\n return { model: candidate.model, path: candidate.path, reason: candidate.reason };\n}\n\n/** Does a caught error match a vector's expected refusal? */\nexport function refusalMatches(params: { expected: { model: string; path: string; reason: string }; error: unknown }): boolean {\n const actual = describeRefusal(params.error);\n return (\n actual !== null &&\n actual.model === params.expected.model &&\n actual.path === params.expected.path &&\n actual.reason === params.expected.reason\n );\n}\n"
|
|
8
|
+
],
|
|
9
|
+
"mappings": "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",
|
|
10
|
+
"debugId": "D0D7C59A47D0228364756E2164756E21",
|
|
11
|
+
"names": []
|
|
12
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The conformance vectors.
|
|
3
|
+
*
|
|
4
|
+
* REWRITE tier (pure, no database): `{ models, scope, call }` must produce
|
|
5
|
+
* exactly `expected` — a rewritten call, a byte-identical passthrough, or a
|
|
6
|
+
* refusal matched on `{ model, path, reason }` (never on message text). Both
|
|
7
|
+
* bindings run every vector through their own pure rewriter.
|
|
8
|
+
*
|
|
9
|
+
* BEHAVIOR tier (live database): a checklist of named, normative scenarios
|
|
10
|
+
* each binding implements as tests against the canonical fixture + seed
|
|
11
|
+
* (`SCOPED_FIXTURE_*`, `SCOPED_SEED`). The ids are the shared contract; a
|
|
12
|
+
* binding's suite must cover every id.
|
|
13
|
+
*/
|
|
14
|
+
import type { ConformanceModelsMap } from "./fixture.ts";
|
|
15
|
+
/** One ORM call in the shared `{ model, method, args }` language. */
|
|
16
|
+
export type ScopedCall = {
|
|
17
|
+
readonly model: string;
|
|
18
|
+
readonly method: string;
|
|
19
|
+
readonly args?: Record<string, unknown>;
|
|
20
|
+
};
|
|
21
|
+
export type ExpectedRefusal = {
|
|
22
|
+
readonly model: string;
|
|
23
|
+
readonly path: string;
|
|
24
|
+
readonly reason: string;
|
|
25
|
+
};
|
|
26
|
+
export type RewriteExpectation = {
|
|
27
|
+
readonly kind: "rewrite";
|
|
28
|
+
readonly call: ScopedCall;
|
|
29
|
+
} | {
|
|
30
|
+
readonly kind: "passthrough";
|
|
31
|
+
} | {
|
|
32
|
+
readonly kind: "refuse";
|
|
33
|
+
readonly refusal: ExpectedRefusal;
|
|
34
|
+
};
|
|
35
|
+
export type RewriteVector = {
|
|
36
|
+
readonly name: string;
|
|
37
|
+
readonly description: string;
|
|
38
|
+
/** Tenancy map for this vector; defaults to `SCOPED_MODELS`. */
|
|
39
|
+
readonly models?: ConformanceModelsMap;
|
|
40
|
+
readonly scope: string | null;
|
|
41
|
+
readonly call: ScopedCall;
|
|
42
|
+
readonly expected: RewriteExpectation;
|
|
43
|
+
};
|
|
44
|
+
export type BehaviorCase = {
|
|
45
|
+
readonly id: string;
|
|
46
|
+
readonly description: string;
|
|
47
|
+
};
|
|
48
|
+
export declare const REWRITE_VECTORS: readonly RewriteVector[];
|
|
49
|
+
/**
|
|
50
|
+
* Live-database scenarios every binding's suite must implement against the
|
|
51
|
+
* canonical fixture + seed, under tenant A unless stated. The id is the
|
|
52
|
+
* contract; each binding's test names the id it covers.
|
|
53
|
+
*/
|
|
54
|
+
export declare const BEHAVIOR_CHECKLIST: readonly BehaviorCase[];
|
|
55
|
+
//# sourceMappingURL=vectors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vectors.d.ts","sourceRoot":"","sources":["../src/vectors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAIzD,qEAAqE;AACrE,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAC1B;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GACvD;IAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAA;CAAE,GAChC;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAA;CAAE,CAAC;AAEnE,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,gEAAgE;IAChE,QAAQ,CAAC,MAAM,CAAC,EAAE,oBAAoB,CAAC;IACvC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B,CAAC;AAaF,eAAO,MAAM,eAAe,EAAE,SAAS,aAAa,EAihBnD,CAAC;AAIF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAAS,YAAY,EAoBrD,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vibeorm/scoped-conformance",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "Binding-agnostic conformance artifact for the tenant-scoped database handle ($scoped): the canonical fixture schema, seed, tenancy map, rewrite vectors and behavior checklist that every binding (VibeORM first-class, Prisma extension) must satisfy",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"orm",
|
|
7
|
+
"typescript",
|
|
8
|
+
"multi-tenancy",
|
|
9
|
+
"tenant-scoping",
|
|
10
|
+
"row-level-security",
|
|
11
|
+
"conformance",
|
|
12
|
+
"test-vectors",
|
|
13
|
+
"vibeorm",
|
|
14
|
+
"prisma"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/vibeorm/vibeorm/tree/master/packages/scoped-conformance#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/vibeorm/vibeorm/issues"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/vibeorm/vibeorm.git",
|
|
23
|
+
"directory": "packages/scoped-conformance"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"author": "VibeORM contributors",
|
|
27
|
+
"type": "module",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18",
|
|
40
|
+
"bun": ">=1.2.0"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
}
|
|
45
|
+
}
|