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

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,50 @@ 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
+ * The type of `db.$withoutTenant()`: `db`'s own type, with raw SQL typed the
77
+ * way the un-extended client types it instead of as always throwing. Every
78
+ * other member, the models and `$transaction` included, has exactly `db`'s
79
+ * type, so code can take either client without the two forming a union
80
+ * TypeScript can't call model methods on.
81
+ */
82
+ export type UnscopedClient<TDb, TClient> = Omit<TDb, RawSqlMethod | '$forOrg' | '$withoutTenant'> & Pick<TClient, Extract<keyof TClient, RawSqlMethod>> & {
83
+ $forOrg(this: ExtendableClient, organizationId: string): TDb;
84
+ $withoutTenant(this: ExtendableClient): UnscopedClient<TDb, TClient>;
85
+ };
60
86
  /**
61
87
  * Builds a Prisma Client extension that scopes every operation on a
62
88
  * tenant-owned model to the tenant id `config.getTenantId()` (or the
63
89
  * default reader of `context.currentOrg?.id`) returns, and adds
64
90
  * `db.$forOrg(organizationId)` / `db.$withoutTenant()` escape hatches.
65
91
  *
92
+ * Every client the escape hatches return carries them too, and
93
+ * `db.$forOrg(id)` has the same type as `db`: TypeScript takes that type from
94
+ * the client the method is called on (`this`). So the tenancy extension
95
+ * should be the last one `db` is extended with; one added after it would be
96
+ * in the type of a `$forOrg` client but not on the client itself.
97
+ *
66
98
  * See `docs/implementation-plans/2026-08-26-multi-tenancy.md` ("Layer 1")
67
99
  * for the full behavior table this implements.
68
100
  */
69
101
  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<{}, {}, {}, {
102
+ $forOrg: <T extends ExtendableClient>(this: T, organizationId: string) => T;
103
+ $withoutTenant: <T extends ExtendableClient>(this: T) => UnscopedClient<T, TClient>;
83
104
  $queryRaw: typeof throwRawQueryBlocked;
84
105
  $queryRawUnsafe: typeof throwRawQueryBlocked;
85
106
  $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":"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;AAQD;;;;;;GAMG;AACH,KAAK,gBAAgB,GAAG;IAAE,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAA;AAQ7C;;;GAGG;AACH,KAAK,YAAY,GACf,WAAW,GAAG,iBAAiB,GAAG,aAAa,GAAG,mBAAmB,CAAA;AAEvE;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,CAAC,GAAG,EAAE,OAAO,IAAI,IAAI,CAC7C,GAAG,EACH,YAAY,GAAG,SAAS,GAAG,gBAAgB,CAC5C,GACC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,OAAO,EAAE,YAAY,CAAC,CAAC,GAAG;IACpD,OAAO,CAAC,IAAI,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,GAAG,GAAG,CAAA;IAC5D,cAAc,CAAC,IAAI,EAAE,gBAAgB,GAAG,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;CACrE,CAAA;AAEH;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAC5C,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC;cA8BX,CAAC,SAAS,gBAAgB,QACnC,CAAC,kBACS,MAAM,KACrB,CAAC;qBAaoB,CAAC,SAAS,gBAAgB,QAC1C,CAAC,KACN,cAAc,CAAC,CAAC,EAAE,OAAO,CAAC;;;;;0DA2DhC"}
@@ -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.3556",
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.3556",
48
+ "@cedarjs/context": "8.0.0-canary.3556",
49
+ "@cedarjs/graphql-server": "8.0.0-canary.3556"
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.3556",
55
+ "@cedarjs/framework-tools": "8.0.0-canary.3556",
56
+ "@cedarjs/router": "8.0.0-canary.3556",
57
+ "@cedarjs/web": "8.0.0-canary.3556",
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.3556",
84
+ "@cedarjs/router": "8.0.0-canary.3556",
85
+ "@cedarjs/web": "8.0.0-canary.3556",
86
86
  "@prisma/client": "7.10.1",
87
87
  "react": "19.3.1"
88
88
  },