@cedarjs/tenancy 8.0.0-canary.3553 → 8.0.0-canary.3557

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -3,5 +3,5 @@ export * from './context.js';
3
3
  export * from './auth.js';
4
4
  export * from './permissions.js';
5
5
  export { createTenancyExtension } from './prismaExtension.js';
6
- export type { ModelNamesFor, TenancyConfig } from './prismaExtension.js';
6
+ export type { ModelNamesFor, TenancyConfig, UnscopedClient, } from './prismaExtension.js';
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,WAAW,CAAA;AACzB,cAAc,kBAAkB,CAAA;AAChC,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAA;AAC7D,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,WAAW,CAAA;AACzB,cAAc,kBAAkB,CAAA;AAChC,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAA;AAC7D,YAAY,EACV,aAAa,EACb,aAAa,EACb,cAAc,GACf,MAAM,sBAAsB,CAAA"}
@@ -57,29 +57,79 @@ interface ParsedModelFields {
57
57
  */
58
58
  export declare function parseListFieldsFromSchema(schemaText: string): Map<string, ParsedModelFields>;
59
59
  declare function throwRawQueryBlocked(): never;
60
+ /**
61
+ * A client the escape hatches can be called on. Prisma's interactive
62
+ * transaction client has no `$extends`, so a client built for it couldn't
63
+ * stay on the transaction's connection; leaving `$extends` out of its type
64
+ * makes `tx.$forOrg()` a type error, and `isTransactionClient` refuses it at
65
+ * runtime.
66
+ */
67
+ type ExtendableClient = {
68
+ $extends: unknown;
69
+ };
70
+ /**
71
+ * The client methods a scoped client blocks (see `throwRawQueryBlocked`) and
72
+ * an unscoped one allows.
73
+ */
74
+ type RawSqlMethod = '$queryRaw' | '$queryRawUnsafe' | '$executeRaw' | '$executeRawUnsafe';
75
+ /**
76
+ * Raw SQL typed the way the un-extended client types it, instead of as
77
+ * always throwing.
78
+ */
79
+ type AllowedRawSql<TClient> = Pick<TClient, Extract<keyof TClient, RawSqlMethod>>;
80
+ /**
81
+ * The `tx` and the options of `TDb`'s interactive `$transaction`, read from
82
+ * Prisma's own signature. A conditional type matches the last overload, which
83
+ * for Prisma is the interactive one (the batch form comes first).
84
+ */
85
+ type InteractiveTransaction<TDb> = TDb extends {
86
+ $transaction: (fn: (tx: infer Tx) => Promise<unknown>, options?: infer Options) => Promise<unknown>;
87
+ } ? {
88
+ tx: Tx;
89
+ options: Options;
90
+ } : never;
91
+ /**
92
+ * The `$transaction` of `db.$withoutTenant()`: its callback's `tx` is `db`'s
93
+ * own transaction client with raw SQL allowed, like the client it was started
94
+ * on (at runtime it's built from that unscoped client). `db`'s own overloads
95
+ * follow it, unchanged: the batch form, and the callback form for a callback
96
+ * that names `db`'s `tx` type. Keeping them also lets TypeScript call
97
+ * `$transaction` on a union of `db` and the unscoped client, using the
98
+ * signatures the two have in common.
99
+ */
100
+ type UnscopedTransaction<TDb, TClient> = (<R>(fn: (tx: Omit<InteractiveTransaction<TDb>['tx'], RawSqlMethod> & AllowedRawSql<TClient>) => Promise<R>, options?: InteractiveTransaction<TDb>['options']) => Promise<R>) & (TDb extends {
101
+ $transaction: infer T;
102
+ } ? T : never);
103
+ /**
104
+ * The type of `db.$withoutTenant()`: `db`'s own type, with raw SQL typed the
105
+ * way the un-extended client types it instead of as always throwing, on the
106
+ * client and on its interactive transactions' `tx`. Every other member, the
107
+ * models included, has exactly `db`'s type, so code can take either client
108
+ * without the two forming a union TypeScript can't call model methods on.
109
+ */
110
+ export type UnscopedClient<TDb, TClient> = Omit<TDb, RawSqlMethod | '$transaction' | '$forOrg' | '$withoutTenant'> & AllowedRawSql<TClient> & {
111
+ $transaction: UnscopedTransaction<TDb, TClient>;
112
+ $forOrg(this: ExtendableClient, organizationId: string): TDb;
113
+ $withoutTenant(this: ExtendableClient): UnscopedClient<TDb, TClient>;
114
+ };
60
115
  /**
61
116
  * Builds a Prisma Client extension that scopes every operation on a
62
117
  * tenant-owned model to the tenant id `config.getTenantId()` (or the
63
118
  * default reader of `context.currentOrg?.id`) returns, and adds
64
119
  * `db.$forOrg(organizationId)` / `db.$withoutTenant()` escape hatches.
65
120
  *
121
+ * Every client the escape hatches return carries them too, and
122
+ * `db.$forOrg(id)` has the same type as `db`: TypeScript takes that type from
123
+ * the client the method is called on (`this`). So the tenancy extension
124
+ * should be the last one `db` is extended with; one added after it would be
125
+ * in the type of a `$forOrg` client but not on the client itself.
126
+ *
66
127
  * See `docs/implementation-plans/2026-08-26-multi-tenancy.md` ("Layer 1")
67
128
  * for the full behavior table this implements.
68
129
  */
69
130
  export declare function createTenancyExtension<TClient>(config: TenancyConfig<TClient>): (client: any) => import("@prisma/client/extension").PrismaClientExtends<import("@prisma/client/runtime/client").InternalArgs<{}, {}, {}, {
70
- /**
71
- * A client scoped to `organizationId` regardless of request
72
- * context — for background jobs, webhooks, and other code that
73
- * knows the tenant but doesn't run inside a request.
74
- */
75
- $forOrg(organizationId: string): TClient;
76
- /**
77
- * An unscoped client for code that intentionally reads or writes
78
- * across organizations: seeds, data migrations, admin tooling.
79
- * Nothing here is scoped, and raw SQL is allowed.
80
- */
81
- $withoutTenant(): TClient;
82
- }> & import("@prisma/client/runtime/client").InternalArgs<{}, {}, {}, {
131
+ $forOrg: <T extends ExtendableClient>(this: T, organizationId: string) => T;
132
+ $withoutTenant: <T extends ExtendableClient>(this: T) => UnscopedClient<T, TClient>;
83
133
  $queryRaw: typeof throwRawQueryBlocked;
84
134
  $queryRawUnsafe: typeof throwRawQueryBlocked;
85
135
  $executeRaw: typeof throwRawQueryBlocked;
@@ -1 +1 @@
1
- {"version":3,"file":"prismaExtension.d.ts","sourceRoot":"","sources":["../src/prismaExtension.ts"],"names":[],"mappings":"AAQA,KAAK,uBAAuB,CAAC,CAAC,IAAI,CAAC,SAAS,IAAI,MAAM,EAAE,GACpD,KAAK,GACL,CAAC,SAAS,MAAM,GACd,KAAK,GACL,CAAC,CAAA;AAEP;;;GAGG;AACH,MAAM,MAAM,aAAa,CAAC,OAAO,IAAI,uBAAuB,CAAC,MAAM,OAAO,CAAC,CAAA;AAE3E,MAAM,WAAW,aAAa,CAAC,OAAO;IACpC;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;OAMG;IACH,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,EAAE,GAAG;QAAE,SAAS,EAAE,aAAa,CAAC,OAAO,CAAC,EAAE,CAAA;KAAE,CAAA;IAC1E;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAA;CACvC;AA4ID;;;;;;GAMG;AACH,UAAU,iBAAiB;IACzB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACxB,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CACxB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,MAAM,GACjB,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAwChC;AAiwBD,iBAAS,oBAAoB,IAAI,KAAK,CAErC;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAC5C,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC;IA6DxB;;;;OAIG;4BACqB,MAAM,GAMiC,OAAO;IAEtE;;;;OAIG;sBAE2B,OAAO;;;;;;0DAK5C"}
1
+ {"version":3,"file":"prismaExtension.d.ts","sourceRoot":"","sources":["../src/prismaExtension.ts"],"names":[],"mappings":"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"}
@@ -504,6 +504,10 @@ const RAW_QUERY_MESSAGE = "Raw SQL is not allowed on a tenant-scoped client. Use
504
504
  function throwRawQueryBlocked() {
505
505
  throw new TenantScopeError(RAW_QUERY_MESSAGE);
506
506
  }
507
+ const TRANSACTION_MESSAGE = "$forOrg() and $withoutTenant() can't be called on a transaction client: the client they return would run its queries outside the transaction. Start the transaction on the client you need instead, e.g. db.$forOrg(organizationId).$transaction(async (tx) => ...).";
508
+ function isTransactionClient(client) {
509
+ return typeof client === "object" && client !== null && !("$extends" in client);
510
+ }
507
511
  function createTenancyExtension(config) {
508
512
  const tenantField = config.tenantField ?? "organizationId";
509
513
  const getTenantId = config.getTenantId ?? (() => getCurrentOrg()?.id);
@@ -514,6 +518,18 @@ function createTenancyExtension(config) {
514
518
  runtimeDataModel
515
519
  );
516
520
  const listFieldsByModel = getListFieldsByModel(client);
521
+ function $forOrg(organizationId) {
522
+ if (isTransactionClient(this)) {
523
+ throw new TenantScopeError(TRANSACTION_MESSAGE);
524
+ }
525
+ return buildScopedClient(() => organizationId);
526
+ }
527
+ function $withoutTenant() {
528
+ if (isTransactionClient(this)) {
529
+ throw new TenantScopeError(TRANSACTION_MESSAGE);
530
+ }
531
+ return unscopedClient;
532
+ }
517
533
  function buildScopedClient(tenantIdSource) {
518
534
  const ctx = {
519
535
  tenantField,
@@ -547,6 +563,8 @@ function createTenancyExtension(config) {
547
563
  $executeRawUnsafe: throwRawQueryBlocked
548
564
  },
549
565
  client: {
566
+ $forOrg,
567
+ $withoutTenant,
550
568
  $queryRaw: throwRawQueryBlocked,
551
569
  $queryRawUnsafe: throwRawQueryBlocked,
552
570
  $executeRaw: throwRawQueryBlocked,
@@ -554,27 +572,11 @@ function createTenancyExtension(config) {
554
572
  }
555
573
  });
556
574
  }
557
- const scopedClient = buildScopedClient(getTenantId);
558
- return scopedClient.$extends({
559
- client: {
560
- /**
561
- * A client scoped to `organizationId` regardless of request
562
- * context — for background jobs, webhooks, and other code that
563
- * knows the tenant but doesn't run inside a request.
564
- */
565
- $forOrg(organizationId) {
566
- return buildScopedClient(() => organizationId);
567
- },
568
- /**
569
- * An unscoped client for code that intentionally reads or writes
570
- * across organizations: seeds, data migrations, admin tooling.
571
- * Nothing here is scoped, and raw SQL is allowed.
572
- */
573
- $withoutTenant() {
574
- return client;
575
- }
576
- }
575
+ const unscopedClient = client.$extends({
576
+ name: "cedarjs-tenancy-unscoped",
577
+ client: { $forOrg, $withoutTenant }
577
578
  });
579
+ return buildScopedClient(getTenantId);
578
580
  });
579
581
  }
580
582
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cedarjs/tenancy",
3
- "version": "8.0.0-canary.3553",
3
+ "version": "8.0.0-canary.3557",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/cedarjs/cedar.git",
@@ -44,17 +44,17 @@
44
44
  "test:watch": "vitest watch"
45
45
  },
46
46
  "dependencies": {
47
- "@cedarjs/api": "8.0.0-canary.3553",
48
- "@cedarjs/context": "8.0.0-canary.3553",
49
- "@cedarjs/graphql-server": "8.0.0-canary.3553"
47
+ "@cedarjs/api": "8.0.0-canary.3557",
48
+ "@cedarjs/context": "8.0.0-canary.3557",
49
+ "@cedarjs/graphql-server": "8.0.0-canary.3557"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@apollo/client": "4.3.5",
53
53
  "@arethetypeswrong/cli": "0.18.5",
54
- "@cedarjs/auth": "8.0.0-canary.3553",
55
- "@cedarjs/framework-tools": "8.0.0-canary.3553",
56
- "@cedarjs/router": "8.0.0-canary.3553",
57
- "@cedarjs/web": "8.0.0-canary.3553",
54
+ "@cedarjs/auth": "8.0.0-canary.3557",
55
+ "@cedarjs/framework-tools": "8.0.0-canary.3557",
56
+ "@cedarjs/router": "8.0.0-canary.3557",
57
+ "@cedarjs/web": "8.0.0-canary.3557",
58
58
  "@prisma/adapter-better-sqlite3": "7.10.1",
59
59
  "@prisma/client": "7.10.1",
60
60
  "@testing-library/dom": "10.4.2",
@@ -80,9 +80,9 @@
80
80
  },
81
81
  "peerDependencies": {
82
82
  "@apollo/client": "4.3.5",
83
- "@cedarjs/auth": "8.0.0-canary.3553",
84
- "@cedarjs/router": "8.0.0-canary.3553",
85
- "@cedarjs/web": "8.0.0-canary.3553",
83
+ "@cedarjs/auth": "8.0.0-canary.3557",
84
+ "@cedarjs/router": "8.0.0-canary.3557",
85
+ "@cedarjs/web": "8.0.0-canary.3557",
86
86
  "@prisma/client": "7.10.1",
87
87
  "react": "19.3.1"
88
88
  },