on-zero 0.12.7 → 0.12.9

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.
Files changed (80) hide show
  1. package/dist/cjs/createPermissions.cjs +7 -10
  2. package/dist/cjs/createPermissions.native.js +6 -7
  3. package/dist/cjs/createPermissions.native.js.map +1 -1
  4. package/dist/cjs/createZeroClient.cjs +1 -1
  5. package/dist/cjs/createZeroClient.native.js +1 -1
  6. package/dist/cjs/createZeroClient.native.js.map +1 -1
  7. package/dist/cjs/helpers/asyncContext.cjs +5 -48
  8. package/dist/cjs/helpers/asyncContext.native.js +3 -48
  9. package/dist/cjs/helpers/asyncContext.native.js.map +1 -1
  10. package/dist/cjs/helpers/createMutators.cjs +6 -5
  11. package/dist/cjs/helpers/createMutators.native.js +6 -5
  12. package/dist/cjs/helpers/createMutators.native.js.map +1 -1
  13. package/dist/cjs/helpers/createMutators.test.cjs +45 -3
  14. package/dist/cjs/helpers/createMutators.test.native.js +59 -3
  15. package/dist/cjs/helpers/createMutators.test.native.js.map +1 -1
  16. package/dist/cjs/helpers/ensureLoggedIn.cjs +2 -4
  17. package/dist/cjs/helpers/ensureLoggedIn.native.js +2 -4
  18. package/dist/cjs/helpers/ensureLoggedIn.native.js.map +1 -1
  19. package/dist/cjs/helpers/mutatorContext.test.cjs +105 -0
  20. package/dist/cjs/helpers/mutatorContext.test.native.js +120 -0
  21. package/dist/cjs/helpers/mutatorContext.test.native.js.map +1 -0
  22. package/dist/cjs/server.cjs +1 -1
  23. package/dist/cjs/server.native.js +1 -1
  24. package/dist/cjs/server.native.js.map +1 -1
  25. package/dist/cjs/zeroRunner.native.js.map +1 -1
  26. package/dist/esm/createPermissions.mjs +7 -10
  27. package/dist/esm/createPermissions.mjs.map +1 -1
  28. package/dist/esm/createPermissions.native.js +6 -7
  29. package/dist/esm/createPermissions.native.js.map +1 -1
  30. package/dist/esm/createZeroClient.mjs +1 -1
  31. package/dist/esm/createZeroClient.mjs.map +1 -1
  32. package/dist/esm/createZeroClient.native.js +1 -1
  33. package/dist/esm/createZeroClient.native.js.map +1 -1
  34. package/dist/esm/helpers/asyncContext.mjs +5 -48
  35. package/dist/esm/helpers/asyncContext.mjs.map +1 -1
  36. package/dist/esm/helpers/asyncContext.native.js +3 -48
  37. package/dist/esm/helpers/asyncContext.native.js.map +1 -1
  38. package/dist/esm/helpers/createMutators.mjs +6 -5
  39. package/dist/esm/helpers/createMutators.mjs.map +1 -1
  40. package/dist/esm/helpers/createMutators.native.js +6 -5
  41. package/dist/esm/helpers/createMutators.native.js.map +1 -1
  42. package/dist/esm/helpers/createMutators.test.mjs +45 -3
  43. package/dist/esm/helpers/createMutators.test.mjs.map +1 -1
  44. package/dist/esm/helpers/createMutators.test.native.js +59 -3
  45. package/dist/esm/helpers/createMutators.test.native.js.map +1 -1
  46. package/dist/esm/helpers/ensureLoggedIn.mjs +2 -4
  47. package/dist/esm/helpers/ensureLoggedIn.mjs.map +1 -1
  48. package/dist/esm/helpers/ensureLoggedIn.native.js +2 -4
  49. package/dist/esm/helpers/ensureLoggedIn.native.js.map +1 -1
  50. package/dist/esm/helpers/mutatorContext.test.mjs +82 -0
  51. package/dist/esm/helpers/mutatorContext.test.mjs.map +1 -0
  52. package/dist/esm/helpers/mutatorContext.test.native.js +94 -0
  53. package/dist/esm/helpers/mutatorContext.test.native.js.map +1 -0
  54. package/dist/esm/server.mjs +1 -1
  55. package/dist/esm/server.mjs.map +1 -1
  56. package/dist/esm/server.native.js +1 -1
  57. package/dist/esm/server.native.js.map +1 -1
  58. package/dist/esm/zeroRunner.mjs.map +1 -1
  59. package/dist/esm/zeroRunner.native.js.map +1 -1
  60. package/package.json +2 -2
  61. package/src/createPermissions.ts +11 -8
  62. package/src/createZeroClient.tsx +1 -1
  63. package/src/helpers/asyncContext.native.ts +10 -76
  64. package/src/helpers/asyncContext.ts +18 -79
  65. package/src/helpers/createMutators.test.ts +49 -3
  66. package/src/helpers/createMutators.ts +11 -7
  67. package/src/helpers/ensureLoggedIn.ts +7 -2
  68. package/src/helpers/mutatorContext.test.ts +96 -0
  69. package/src/server.ts +1 -1
  70. package/src/zeroRunner.ts +6 -0
  71. package/types/createPermissions.d.ts +2 -2
  72. package/types/createPermissions.d.ts.map +1 -1
  73. package/types/helpers/asyncContext.d.ts.map +1 -1
  74. package/types/helpers/asyncContext.native.d.ts.map +1 -1
  75. package/types/helpers/createMutators.d.ts +3 -3
  76. package/types/helpers/createMutators.d.ts.map +1 -1
  77. package/types/helpers/ensureLoggedIn.d.ts.map +1 -1
  78. package/types/helpers/mutatorContext.test.d.ts +2 -0
  79. package/types/helpers/mutatorContext.test.d.ts.map +1 -0
  80. package/types/zeroRunner.d.ts.map +1 -1
@@ -1,5 +1,4 @@
1
1
  import { ensure } from './helpers/ensure'
2
- import { mutatorContext } from './helpers/mutatorContext'
3
2
  import { prettyFormatZeroQuery } from './helpers/prettyFormatZeroQuery'
4
3
  import { getZQL } from './state'
5
4
  import { getWhereTableName } from './where'
@@ -95,17 +94,21 @@ export function createPermissions<Schema extends ZeroSchema>({
95
94
  return eb.and(permissionReturn, ...primaryKeyWheres)
96
95
  }
97
96
 
98
- const can: Can = async (where, obj) => {
99
- // on client we always allow! we only check on server (like zero does)
100
- if (environment === 'server') {
101
- const ctx = mutatorContext()
97
+ // bound to the transaction that will call it, never read from ambient state.
98
+ // an ambient lookup checks permissions against whichever mutator happens to be
99
+ // in flight, and only a real AsyncLocalStorage makes that the calling one — a
100
+ // browser-hosted server has none.
101
+ const bindCan =
102
+ (tx: Transaction, authData: AuthData | null): Can =>
103
+ async (where, obj) => {
104
+ // on client we always allow! we only check on server (like zero does)
105
+ if (environment !== 'server') return
102
106
  const tableName = getWhereTableName(where)
103
107
  if (!tableName) {
104
108
  throw new Error(`Must use where('table') style where to pass to can()`)
105
109
  }
106
- await ensurePermission(ctx.tx, ctx.authData, tableName, where, obj)
110
+ await ensurePermission(tx, authData, tableName, where, obj)
107
111
  }
108
- }
109
112
 
110
113
  async function ensurePermission(
111
114
  tx: Transaction,
@@ -148,7 +151,7 @@ export function createPermissions<Schema extends ZeroSchema>({
148
151
  }
149
152
 
150
153
  return {
151
- can,
154
+ bindCan,
152
155
  buildPermissionQuery,
153
156
  }
154
157
  }
@@ -304,7 +304,7 @@ export function createZeroClientInternal<
304
304
  models,
305
305
  environment: 'client',
306
306
  authData: null,
307
- can: permissionsHelpers.can,
307
+ bindCan: permissionsHelpers.bindCan,
308
308
  rollups,
309
309
  })
310
310
  return clientMutators
@@ -5,86 +5,20 @@ interface AsyncContext<T> {
5
5
 
6
6
  export function setupAsyncLocalStorage(_AsyncLocalStorage: unknown): void {}
7
7
 
8
- // react native implementation - no node:async_hooks available
8
+ // react native has no node:async_hooks, and an ambient mutator context cannot
9
+ // be emulated soundly without it: a module-level "current context" is visible
10
+ // to every other task that runs while a mutator awaits, and patching
11
+ // Promise.prototype does not follow a native `await` at all. so there is no
12
+ // ambient mutator context here — createMutators passes each mutator its own
13
+ // context, and transactional reads go through that transaction
14
+ // (`tx.run(zql...)`). see asyncContext.ts for the same reasoning on the web.
9
15
  export function createAsyncContext<T>(): AsyncContext<T> {
10
- let currentContext: T | undefined
11
- const contextStack: (T | undefined)[] = []
12
-
13
16
  return {
14
17
  get(): T | undefined {
15
- return currentContext
18
+ return undefined
16
19
  },
17
- async run<R>(value: T, fn: () => R | Promise<R>): Promise<R> {
18
- const prevContext = currentContext
19
- currentContext = value
20
- contextStack.push(prevContext)
21
-
22
- // store original Promise methods
23
- const OriginalPromise = Promise
24
- const OriginalThen = OriginalPromise.prototype.then
25
- const OriginalCatch = OriginalPromise.prototype.catch
26
- const OriginalFinally = OriginalPromise.prototype.finally
27
-
28
- function wrapCallback(
29
- callback: Function | undefined | null,
30
- context: T | undefined
31
- ): Function | undefined | null {
32
- if (!callback) return callback
33
- return (...args: any[]) => {
34
- const prevContext = currentContext
35
- currentContext = context
36
- try {
37
- return callback(...args)
38
- } finally {
39
- currentContext = prevContext
40
- }
41
- }
42
- }
43
-
44
- // patch Promise methods to capture and restore context
45
- // eslint-disable-next-line no-then-property -- intentional patching for context propagation
46
- OriginalPromise.prototype.then = function (
47
- this: Promise<any>,
48
- onFulfilled?: any,
49
- onRejected?: any
50
- ): Promise<any> {
51
- const context = currentContext
52
- return OriginalThen.call(
53
- this,
54
- wrapCallback(onFulfilled, context) as any,
55
- wrapCallback(onRejected, context) as any
56
- )
57
- }
58
-
59
- OriginalPromise.prototype.catch = function (
60
- this: Promise<any>,
61
- onRejected?: any
62
- ): Promise<any> {
63
- const context = currentContext
64
- return OriginalCatch.call(this, wrapCallback(onRejected, context) as any)
65
- }
66
-
67
- OriginalPromise.prototype.finally = function (
68
- this: Promise<any>,
69
- onFinally?: any
70
- ): Promise<any> {
71
- const context = currentContext
72
- return OriginalFinally.call(this, wrapCallback(onFinally, context) as any)
73
- }
74
-
75
- try {
76
- const result = await fn()
77
- return result
78
- } finally {
79
- // restore original Promise methods
80
- // eslint-disable-next-line no-then-property -- restoring original methods
81
- OriginalPromise.prototype.then = OriginalThen
82
- OriginalPromise.prototype.catch = OriginalCatch
83
- OriginalPromise.prototype.finally = OriginalFinally
84
-
85
- contextStack.pop()
86
- currentContext = prevContext
87
- }
20
+ async run<R>(_value: T, fn: () => R | Promise<R>): Promise<R> {
21
+ return await fn()
88
22
  },
89
23
  }
90
24
  }
@@ -69,91 +69,30 @@ export function createAsyncContext<T>(): AsyncContext<T> {
69
69
  return storage.run(value, fn)
70
70
  },
71
71
  }
72
- } else {
73
- // browser implementation using promise patching
74
- return createBrowserAsyncContext<T>()
75
72
  }
76
- }
77
73
 
78
- function createBrowserAsyncContext<T>(): AsyncContext<T> {
79
- let currentContext: T | undefined
80
- const contextStack: (T | undefined)[] = []
74
+ return createJsRuntimeAsyncContext<T>()
75
+ }
81
76
 
77
+ // browsers and the react-native JS runtime have no AsyncLocalStorage, and an
78
+ // ambient mutator context cannot be emulated soundly on them. a module-level
79
+ // "current context" is visible to every other task that runs while a mutator
80
+ // awaits, and patching Promise.prototype does not follow a native `await` at
81
+ // all. the emulation that used to live here handed a mutator a *different*
82
+ // mutator's transaction whenever two overlapped, and left Promise.prototype
83
+ // patched for the life of the page once two runs interleaved.
84
+ //
85
+ // so on these runtimes there is no ambient mutator context. createMutators
86
+ // already passes each mutator its own context, and transactional reads go
87
+ // through that transaction (`tx.run(zql...)`) rather than through ambient
88
+ // state.
89
+ function createJsRuntimeAsyncContext<T>(): AsyncContext<T> {
82
90
  return {
83
91
  get(): T | undefined {
84
- return currentContext
92
+ return undefined
85
93
  },
86
- async run<R>(value: T, fn: () => R | Promise<R>): Promise<R> {
87
- const prevContext = currentContext
88
- currentContext = value
89
- contextStack.push(prevContext)
90
-
91
- // store original Promise methods
92
- const OriginalPromise = Promise
93
- const OriginalThen = OriginalPromise.prototype.then
94
- const OriginalCatch = OriginalPromise.prototype.catch
95
- const OriginalFinally = OriginalPromise.prototype.finally
96
-
97
- function wrapCallback(
98
- callback: Function | undefined | null,
99
- context: T | undefined
100
- ): Function | undefined | null {
101
- if (!callback) return callback
102
- return (...args: any[]) => {
103
- const prevContext = currentContext
104
- currentContext = context
105
- try {
106
- return callback(...args)
107
- } finally {
108
- currentContext = prevContext
109
- }
110
- }
111
- }
112
-
113
- // patch Promise methods to capture and restore context
114
- // eslint-disable-next-line no-then-property -- intentional patching for context propagation
115
- OriginalPromise.prototype.then = function (
116
- this: Promise<any>,
117
- onFulfilled?: any,
118
- onRejected?: any
119
- ): Promise<any> {
120
- const context = currentContext
121
- return OriginalThen.call(
122
- this,
123
- wrapCallback(onFulfilled, context) as any,
124
- wrapCallback(onRejected, context) as any
125
- )
126
- }
127
-
128
- OriginalPromise.prototype.catch = function (
129
- this: Promise<any>,
130
- onRejected?: any
131
- ): Promise<any> {
132
- const context = currentContext
133
- return OriginalCatch.call(this, wrapCallback(onRejected, context) as any)
134
- }
135
-
136
- OriginalPromise.prototype.finally = function (
137
- this: Promise<any>,
138
- onFinally?: any
139
- ): Promise<any> {
140
- const context = currentContext
141
- return OriginalFinally.call(this, wrapCallback(onFinally, context) as any)
142
- }
143
-
144
- try {
145
- const result = await fn()
146
- return result
147
- } finally {
148
- // restore original Promise methods
149
- // eslint-disable-next-line no-then-property -- restoring original methods
150
- OriginalPromise.prototype.then = OriginalThen
151
- OriginalPromise.prototype.catch = OriginalCatch
152
- OriginalPromise.prototype.finally = OriginalFinally
153
-
154
- contextStack.pop()
155
- currentContext = prevContext
156
- }
94
+ async run<R>(_value: T, fn: () => R | Promise<R>): Promise<R> {
95
+ return await fn()
157
96
  },
158
97
  }
159
98
  }
@@ -15,7 +15,7 @@ describe('createMutators timeout guard', () => {
15
15
  const mutators = createMutators({
16
16
  environment: 'server',
17
17
  authData: null,
18
- can: {} as never,
18
+ bindCan: () => async () => {},
19
19
  models: {
20
20
  task: {
21
21
  mutate: {
@@ -35,7 +35,7 @@ describe('createMutators timeout guard', () => {
35
35
  const mutators = createMutators({
36
36
  environment: 'server',
37
37
  authData: null,
38
- can: {} as never,
38
+ bindCan: () => async () => {},
39
39
  models: {
40
40
  task: {
41
41
  mutate: {
@@ -121,7 +121,7 @@ test('createMutators installs optimistic rollups on client transactions', async
121
121
  const mutators = createMutators({
122
122
  environment: 'client',
123
123
  authData: null,
124
- can: async () => {},
124
+ bindCan: () => async () => {},
125
125
  rollups,
126
126
  models: {
127
127
  comment: {
@@ -141,3 +141,49 @@ test('createMutators installs optimistic rollups on client transactions', async
141
141
 
142
142
  expect(rows.post).toEqual([{ id: 'p1', commentCount: 1 }])
143
143
  })
144
+
145
+ test('each mutator permission check runs against its own transaction', async () => {
146
+ // two mutations overlap, which is the ordinary case anywhere several writes
147
+ // are in flight. a permission check that reached for an ambient context would
148
+ // check one mutator's rows through the other's transaction, and on a
149
+ // browser-hosted server (no AsyncLocalStorage) it always would.
150
+ const checked: Array<{ mutation: string; tx: string }> = []
151
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
152
+
153
+ const mutators = createMutators({
154
+ environment: 'server',
155
+ authData: null,
156
+ bindCan: (tx) => async (_where, obj) => {
157
+ checked.push({
158
+ mutation: String(obj),
159
+ tx: (tx as unknown as { name: string }).name,
160
+ })
161
+ },
162
+ models: {
163
+ task: {
164
+ mutate: {
165
+ touch: async (ctx, input: { label: string; delay: number }) => {
166
+ await sleep(input.delay)
167
+ await ctx.can({} as never, input.label)
168
+ },
169
+ },
170
+ },
171
+ } as never,
172
+ }) as any
173
+
174
+ await Promise.all([
175
+ Reflect.apply(mutators.task.touch, null, [
176
+ { name: 'tx-A' },
177
+ { label: 'A', delay: 20 },
178
+ ]),
179
+ Reflect.apply(mutators.task.touch, null, [
180
+ { name: 'tx-B' },
181
+ { label: 'B', delay: 5 },
182
+ ]),
183
+ ])
184
+
185
+ expect(checked.sort((l, r) => l.mutation.localeCompare(r.mutation))).toEqual([
186
+ { mutation: 'A', tx: 'tx-A' },
187
+ { mutation: 'B', tx: 'tx-B' },
188
+ ])
189
+ })
@@ -30,7 +30,7 @@ export function createMutators<Models extends GenericModels>({
30
30
  createServerActions,
31
31
  enqueueTask = () => {},
32
32
  enqueueAction = () => {},
33
- can,
33
+ bindCan,
34
34
  models,
35
35
  validateMutation,
36
36
  mutationValidators,
@@ -39,7 +39,7 @@ export function createMutators<Models extends GenericModels>({
39
39
  }: {
40
40
  environment: 'server' | 'client'
41
41
  authData: AuthData | null
42
- can: Can
42
+ bindCan: (tx: Transaction, authData: AuthData | null) => Can
43
43
  models: Models
44
44
  enqueueTask?: NonNullable<MutatorContext['server']>['enqueueTask']
45
45
  enqueueAction?: NonNullable<MutatorContext['server']>['enqueueAction']
@@ -61,14 +61,18 @@ export function createMutators<Models extends GenericModels>({
61
61
  return async (tx: Transaction, ...args: Args): Promise<void> => {
62
62
  const transaction =
63
63
  environment === 'client' && rollups ? withOptimisticRollups(tx, rollups) : tx
64
+ // on client, read authData dynamically to avoid stale closure during auth
65
+ // transitions (ZeroProvider recreates Zero instance in useEffect, but
66
+ // mutations can run before that)
67
+ const contextAuthData =
68
+ environment === 'client' ? getAuthData() : (resolveAuthData?.() ?? authData)
64
69
  const mutationContext: MutatorContext = {
65
70
  tx: transaction,
66
- // on client, read authData dynamically to avoid stale closure during auth transitions
67
- // (ZeroProvider recreates Zero instance in useEffect, but mutations can run before that)
68
- authData:
69
- environment === 'client' ? getAuthData() : (resolveAuthData?.() ?? authData),
71
+ authData: contextAuthData,
70
72
  environment,
71
- can,
73
+ // bound to THIS transaction. a permission check must run against the
74
+ // mutator that asked for it, and ambient lookup cannot promise that.
75
+ can: bindCan(transaction, contextAuthData),
72
76
  server:
73
77
  environment === 'server'
74
78
  ? ({
@@ -1,10 +1,15 @@
1
1
  import { ensure } from './ensure'
2
- import { mutatorContext } from './mutatorContext'
2
+ import { getAuth } from './getAuth'
3
3
 
4
4
  import type { AuthData } from '../types'
5
5
 
6
+ // the mutator context is ambient only where the runtime can scope it, which is
7
+ // the server. on the client and in react native a mutator's authData is the
8
+ // same value getAuth() resolves — createMutators seeds the client context from
9
+ // it — so reading through getAuth keeps this working in mutators on every
10
+ // environment.
6
11
  export const ensureLoggedIn = (): AuthData => {
7
- const { authData } = mutatorContext()
12
+ const authData = getAuth()
8
13
  ensure(authData, 'logged in')
9
14
  return authData
10
15
  }
@@ -0,0 +1,96 @@
1
+ // force the non-server runtime before anything reads platform.ts, so this file
2
+ // exercises the browser / react-native path rather than AsyncLocalStorage.
3
+ process.env.VITE_ENVIRONMENT = 'client'
4
+
5
+ import { beforeEach, describe, expect, test } from 'vitest'
6
+
7
+ import { setAuthData } from '../state'
8
+
9
+ const store = globalThis as typeof globalThis & {
10
+ __onZeroMutatorContext__?: unknown
11
+ __onZeroAuthScopeContext__?: unknown
12
+ }
13
+
14
+ beforeEach(() => {
15
+ store.__onZeroMutatorContext__ = undefined
16
+ store.__onZeroAuthScopeContext__ = undefined
17
+ })
18
+
19
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
20
+
21
+ const fakeContext = (name: string) =>
22
+ ({
23
+ tx: { name } as never,
24
+ authData: null,
25
+ environment: 'client',
26
+ can: {} as never,
27
+ }) as never
28
+
29
+ describe('mutator context on runtimes without AsyncLocalStorage', () => {
30
+ test('a task running alongside a mutator is never told it is in a mutation', async () => {
31
+ const { isInZeroMutation, runWithContext } = await import('./mutatorContext')
32
+
33
+ // stands in for agentQaRunner: ordinary async work on its own timeline that
34
+ // asks "am I inside a mutation?" to decide how to run a query. it started
35
+ // before any mutator and belongs to no transaction.
36
+ const observations: boolean[] = []
37
+ const bystander = (async () => {
38
+ for (let i = 0; i < 6; i++) {
39
+ await sleep(5)
40
+ observations.push(isInZeroMutation())
41
+ }
42
+ })()
43
+
44
+ await sleep(8)
45
+ await runWithContext(fakeContext('A'), async () => {
46
+ await sleep(20)
47
+ })
48
+ await bystander
49
+
50
+ expect(observations).toHaveLength(6)
51
+ expect(observations.filter(Boolean)).toEqual([])
52
+ })
53
+
54
+ test('overlapping mutators leave Promise.prototype alone', async () => {
55
+ const { runWithContext } = await import('./mutatorContext')
56
+ const pristine = {
57
+ then: Promise.prototype.then,
58
+ catch: Promise.prototype.catch,
59
+ finally: Promise.prototype.finally,
60
+ }
61
+
62
+ const first = runWithContext(fakeContext('A'), () => sleep(20))
63
+ await sleep(5)
64
+ const second = runWithContext(fakeContext('B'), () => sleep(20))
65
+ await Promise.all([first, second])
66
+
67
+ expect(Promise.prototype.then).toBe(pristine.then)
68
+ expect(Promise.prototype.catch).toBe(pristine.catch)
69
+ expect(Promise.prototype.finally).toBe(pristine.finally)
70
+ })
71
+
72
+ test('runWithContext still returns the function result and propagates throws', async () => {
73
+ const { runWithContext } = await import('./mutatorContext')
74
+
75
+ await expect(runWithContext(fakeContext('A'), async () => 'ok')).resolves.toBe('ok')
76
+ await expect(
77
+ runWithContext(fakeContext('A'), async () => {
78
+ throw new Error('boom')
79
+ })
80
+ ).rejects.toThrow('boom')
81
+ })
82
+
83
+ test('ensureLoggedIn works inside a mutator without an ambient context', async () => {
84
+ const { runWithContext } = await import('./mutatorContext')
85
+ const { ensureLoggedIn } = await import('./ensureLoggedIn')
86
+
87
+ setAuthData({ id: 'user_1' } as never)
88
+
89
+ const auth = await runWithContext(fakeContext('A'), async () => {
90
+ await sleep(5)
91
+ return ensureLoggedIn()
92
+ })
93
+
94
+ expect(auth.id).toBe('user_1')
95
+ })
96
+ })
package/src/server.ts CHANGED
@@ -305,7 +305,7 @@ export function createZeroServerBindings<
305
305
  }
306
306
  const decoratedMutators = createMutators({
307
307
  authData: null,
308
- can: permissions.can,
308
+ bindCan: permissions.bindCan,
309
309
  createServerActions: options.createServerActions,
310
310
  environment: 'server',
311
311
  models: options.models,
package/src/zeroRunner.ts CHANGED
@@ -21,6 +21,12 @@ export function setRunner(r: ZeroRunner | null) {
21
21
  }
22
22
 
23
23
  export function getRunner(instance?: { runner: ZeroRunner | null }): ZeroRunner {
24
+ // only the server has a real async context, so only there can "am I inside a
25
+ // mutator" be answered for the CALLER rather than for the page. on the client
26
+ // isInZeroMutation() is always false by construction (see asyncContext.ts):
27
+ // ordinary async code that happens to overlap a mutator must not be handed
28
+ // that mutator's transaction, and a client mutator that needs a transactional
29
+ // read uses its own `tx.run(zql...)`.
24
30
  if (isInZeroMutation()) {
25
31
  return (q, o) => mutatorContext().tx.run(q, o)
26
32
  }
@@ -1,4 +1,4 @@
1
- import type { AdminRoleMode, AuthData, Can, TableName, Where } from './types';
1
+ import type { AdminRoleMode, AuthData, Can, TableName, Transaction, Where } from './types';
2
2
  import type { Condition, ExpressionBuilder, Schema as ZeroSchema } from '@rocicorp/zero';
3
3
  export declare class PermissionError extends Error {
4
4
  constructor(message: string);
@@ -8,7 +8,7 @@ export declare function createPermissions<Schema extends ZeroSchema>({ environme
8
8
  schema: Schema;
9
9
  adminRoleMode?: AdminRoleMode;
10
10
  }): {
11
- can: Can;
11
+ bindCan: (tx: Transaction, authData: AuthData | null) => Can;
12
12
  buildPermissionQuery: <PermissionWhere extends Where<string>>(authData: AuthData | null, eb: ExpressionBuilder<any, any>, permissionWhere: PermissionWhere, objOrId: Record<string, any> | string, tableNameOverride?: TableName) => Condition;
13
13
  };
14
14
  //# sourceMappingURL=createPermissions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"createPermissions.d.ts","sourceRoot":"","sources":["../src/createPermissions.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,EAAe,KAAK,EAAE,MAAM,SAAS,CAAA;AAC1F,OAAO,KAAK,EACV,SAAS,EACT,iBAAiB,EAEjB,MAAM,IAAI,UAAU,EACrB,MAAM,gBAAgB,CAAA;AAEvB,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAI5B;AAED,wBAAgB,iBAAiB,CAAC,MAAM,SAAS,UAAU,EAAE,EAC3D,WAAW,EACX,MAAM,EACN,aAAqB,GACtB,EAAE;IACD,WAAW,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAChC,MAAM,EAAE,MAAM,CAAA;IACd,aAAa,CAAC,EAAE,aAAa,CAAA;CAC9B;;2BAG+B,eAAe,kCACjC,QAAQ,GAAG,IAAI,MACrB,iBAAiB,CAAC,GAAG,EAAE,GAAG,CAAC,mBACd,eAAe,WAEvB,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,sBACjB,SAAS;EAmHhC"}
1
+ {"version":3,"file":"createPermissions.d.ts","sourceRoot":"","sources":["../src/createPermissions.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAC1F,OAAO,KAAK,EACV,SAAS,EACT,iBAAiB,EAEjB,MAAM,IAAI,UAAU,EACrB,MAAM,gBAAgB,CAAA;AAEvB,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAI5B;AAED,wBAAgB,iBAAiB,CAAC,MAAM,SAAS,UAAU,EAAE,EAC3D,WAAW,EACX,MAAM,EACN,aAAqB,GACtB,EAAE;IACD,WAAW,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAChC,MAAM,EAAE,MAAM,CAAA;IACd,aAAa,CAAC,EAAE,aAAa,CAAA;CAC9B;kBAyEQ,WAAW,YAAY,QAAQ,GAAG,IAAI,KAAG,GAAG;2BAtErB,eAAe,kCACjC,QAAQ,GAAG,IAAI,MACrB,iBAAiB,CAAC,GAAG,EAAE,GAAG,CAAC,mBACd,eAAe,WAEvB,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,sBACjB,SAAS;EAuHhC"}
@@ -1 +1 @@
1
- {"version":3,"file":"asyncContext.d.ts","sourceRoot":"","sources":["../../src/helpers/asyncContext.ts"],"names":[],"mappings":"AAEA,UAAU,YAAY,CAAC,CAAC;IACtB,GAAG,IAAI,CAAC,GAAG,SAAS,CAAA;IACpB,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;CACvD;AAED,UAAU,qBAAqB,CAAC,CAAC;IAC/B,QAAQ,IAAI,CAAC,GAAG,SAAS,CAAA;IACzB,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,CAAA;CACvC;AAED,UAAU,4BAA4B;IACpC,KAAK,CAAC,KAAK,qBAAqB,CAAC,CAAC,CAAC,CAAA;CACpC;AAQD,wBAAgB,sBAAsB,CACpC,iBAAiB,EAAE,4BAA4B,GAAG,IAAI,GACrD,IAAI,CAEN;AAeD,wBAAgB,kBAAkB,CAAC,CAAC,KAAK,YAAY,CAAC,CAAC,CAAC,CAkCvD"}
1
+ {"version":3,"file":"asyncContext.d.ts","sourceRoot":"","sources":["../../src/helpers/asyncContext.ts"],"names":[],"mappings":"AAEA,UAAU,YAAY,CAAC,CAAC;IACtB,GAAG,IAAI,CAAC,GAAG,SAAS,CAAA;IACpB,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;CACvD;AAED,UAAU,qBAAqB,CAAC,CAAC;IAC/B,QAAQ,IAAI,CAAC,GAAG,SAAS,CAAA;IACzB,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,CAAA;CACvC;AAED,UAAU,4BAA4B;IACpC,KAAK,CAAC,KAAK,qBAAqB,CAAC,CAAC,CAAC,CAAA;CACpC;AAQD,wBAAgB,sBAAsB,CACpC,iBAAiB,EAAE,4BAA4B,GAAG,IAAI,GACrD,IAAI,CAEN;AAeD,wBAAgB,kBAAkB,CAAC,CAAC,KAAK,YAAY,CAAC,CAAC,CAAC,CAiCvD"}
@@ -1 +1 @@
1
- {"version":3,"file":"asyncContext.native.d.ts","sourceRoot":"","sources":["../../src/helpers/asyncContext.native.ts"],"names":[],"mappings":"AAAA,UAAU,YAAY,CAAC,CAAC;IACtB,GAAG,IAAI,CAAC,GAAG,SAAS,CAAA;IACpB,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;CACvD;AAED,wBAAgB,sBAAsB,CAAC,kBAAkB,EAAE,OAAO,GAAG,IAAI,CAAG;AAG5E,wBAAgB,kBAAkB,CAAC,CAAC,KAAK,YAAY,CAAC,CAAC,CAAC,CAiFvD"}
1
+ {"version":3,"file":"asyncContext.native.d.ts","sourceRoot":"","sources":["../../src/helpers/asyncContext.native.ts"],"names":[],"mappings":"AAAA,UAAU,YAAY,CAAC,CAAC;IACtB,GAAG,IAAI,CAAC,GAAG,SAAS,CAAA;IACpB,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;CACvD;AAED,wBAAgB,sBAAsB,CAAC,kBAAkB,EAAE,OAAO,GAAG,IAAI,CAAG;AAS5E,wBAAgB,kBAAkB,CAAC,CAAC,KAAK,YAAY,CAAC,CAAC,CAAC,CASvD"}
@@ -1,4 +1,4 @@
1
- import type { AuthData, Can, GenericModels, GetZeroMutators, MutatorContext } from '../types';
1
+ import type { AuthData, Can, GenericModels, GetZeroMutators, MutatorContext, Transaction } from '../types';
2
2
  import type { RollupSet } from 'orez-lite/rollup';
3
3
  export type ValidateMutationFn = (args: {
4
4
  authData: AuthData | null;
@@ -7,10 +7,10 @@ export type ValidateMutationFn = (args: {
7
7
  args: unknown;
8
8
  }) => void | Promise<void>;
9
9
  export type { ValidateMutationFn as CreateMutatorsValidateFn };
10
- export declare function createMutators<Models extends GenericModels>({ environment, authData, createServerActions, enqueueTask, enqueueAction, can, models, validateMutation, mutationValidators, resolveAuthData, rollups, }: {
10
+ export declare function createMutators<Models extends GenericModels>({ environment, authData, createServerActions, enqueueTask, enqueueAction, bindCan, models, validateMutation, mutationValidators, resolveAuthData, rollups, }: {
11
11
  environment: 'server' | 'client';
12
12
  authData: AuthData | null;
13
- can: Can;
13
+ bindCan: (tx: Transaction, authData: AuthData | null) => Can;
14
14
  models: Models;
15
15
  enqueueTask?: NonNullable<MutatorContext['server']>['enqueueTask'];
16
16
  enqueueAction?: NonNullable<MutatorContext['server']>['enqueueAction'];
@@ -1 +1 @@
1
- {"version":3,"file":"createMutators.d.ts","sourceRoot":"","sources":["../../src/helpers/createMutators.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,QAAQ,EACR,GAAG,EACH,aAAa,EACb,eAAe,EACf,cAAc,EAEf,MAAM,UAAU,CAAA;AACjB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAEjD,MAAM,MAAM,kBAAkB,GAAG,CAAC,IAAI,EAAE;IACtC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAA;IACzB,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,OAAO,CAAA;CACd,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;AAE1B,YAAY,EAAE,kBAAkB,IAAI,wBAAwB,EAAE,CAAA;AAE9D,wBAAgB,cAAc,CAAC,MAAM,SAAS,aAAa,EAAE,EAC3D,WAAW,EACX,QAAQ,EACR,mBAAmB,EACnB,WAAsB,EACtB,aAAwB,EACxB,GAAG,EACH,MAAM,EACN,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,OAAO,GACR,EAAE;IACD,WAAW,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAChC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAA;IACzB,GAAG,EAAE,GAAG,CAAA;IACR,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,WAAW,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,aAAa,CAAC,CAAA;IAClE,aAAa,CAAC,EAAE,WAAW,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,eAAe,CAAC,CAAA;IACtE,mBAAmB,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC/C,gBAAgB,CAAC,EAAE,kBAAkB,CAAA;IACrC,sFAAsF;IACtF,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAA;IACxD,eAAe,CAAC,EAAE,MAAM,QAAQ,GAAG,IAAI,CAAA;IACvC,OAAO,CAAC,EAAE,SAAS,CAAA;CACpB,GAAG,eAAe,CAAC,MAAM,CAAC,CA4K1B"}
1
+ {"version":3,"file":"createMutators.d.ts","sourceRoot":"","sources":["../../src/helpers/createMutators.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,QAAQ,EACR,GAAG,EACH,aAAa,EACb,eAAe,EACf,cAAc,EACd,WAAW,EACZ,MAAM,UAAU,CAAA;AACjB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAEjD,MAAM,MAAM,kBAAkB,GAAG,CAAC,IAAI,EAAE;IACtC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAA;IACzB,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,OAAO,CAAA;CACd,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;AAE1B,YAAY,EAAE,kBAAkB,IAAI,wBAAwB,EAAE,CAAA;AAE9D,wBAAgB,cAAc,CAAC,MAAM,SAAS,aAAa,EAAE,EAC3D,WAAW,EACX,QAAQ,EACR,mBAAmB,EACnB,WAAsB,EACtB,aAAwB,EACxB,OAAO,EACP,MAAM,EACN,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,OAAO,GACR,EAAE;IACD,WAAW,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAChC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAA;IACzB,OAAO,EAAE,CAAC,EAAE,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,KAAK,GAAG,CAAA;IAC5D,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,WAAW,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,aAAa,CAAC,CAAA;IAClE,aAAa,CAAC,EAAE,WAAW,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,eAAe,CAAC,CAAA;IACtE,mBAAmB,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC/C,gBAAgB,CAAC,EAAE,kBAAkB,CAAA;IACrC,sFAAsF;IACtF,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAA;IACxD,eAAe,CAAC,EAAE,MAAM,QAAQ,GAAG,IAAI,CAAA;IACvC,OAAO,CAAC,EAAE,SAAS,CAAA;CACpB,GAAG,eAAe,CAAC,MAAM,CAAC,CAgL1B"}
@@ -1 +1 @@
1
- {"version":3,"file":"ensureLoggedIn.d.ts","sourceRoot":"","sources":["../../src/helpers/ensureLoggedIn.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAExC,eAAO,MAAM,cAAc,QAAO,QAIjC,CAAA"}
1
+ {"version":3,"file":"ensureLoggedIn.d.ts","sourceRoot":"","sources":["../../src/helpers/ensureLoggedIn.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAOxC,eAAO,MAAM,cAAc,QAAO,QAIjC,CAAA"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=mutatorContext.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mutatorContext.test.d.ts","sourceRoot":"","sources":["../../src/helpers/mutatorContext.test.ts"],"names":[],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"zeroRunner.d.ts","sourceRoot":"","sources":["../src/zeroRunner.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,aAAa,EACb,KAAK,EACL,UAAU,EACV,MAAM,IAAI,UAAU,EACrB,MAAM,gBAAgB,CAAA;AAEvB,YAAY,EAAE,UAAU,EAAE,CAAA;AAE1B,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,EAC/B,KAAK,EAAE,KAAK,CAAC,GAAG,EAAE,UAAU,EAAE,OAAO,CAAC,EACtC,OAAO,CAAC,EAAE,UAAU,KACjB,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAA;AAIpC,wBAAgB,SAAS,CAAC,CAAC,EAAE,UAAU,GAAG,IAAI,QAE7C;AAED,wBAAgB,SAAS,CAAC,QAAQ,CAAC,EAAE;IAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CAAA;CAAE,GAAG,UAAU,CAM9E;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,CAAC,EAAE;IAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CAAA;CAAE,GAAG,UAAU,CAcrF"}
1
+ {"version":3,"file":"zeroRunner.d.ts","sourceRoot":"","sources":["../src/zeroRunner.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,aAAa,EACb,KAAK,EACL,UAAU,EACV,MAAM,IAAI,UAAU,EACrB,MAAM,gBAAgB,CAAA;AAEvB,YAAY,EAAE,UAAU,EAAE,CAAA;AAE1B,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,EAC/B,KAAK,EAAE,KAAK,CAAC,GAAG,EAAE,UAAU,EAAE,OAAO,CAAC,EACtC,OAAO,CAAC,EAAE,UAAU,KACjB,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAA;AAIpC,wBAAgB,SAAS,CAAC,CAAC,EAAE,UAAU,GAAG,IAAI,QAE7C;AAED,wBAAgB,SAAS,CAAC,QAAQ,CAAC,EAAE;IAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CAAA;CAAE,GAAG,UAAU,CAY9E;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,CAAC,EAAE;IAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CAAA;CAAE,GAAG,UAAU,CAcrF"}