on-zero 0.12.7 → 0.12.8
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/cjs/createPermissions.cjs +7 -10
- package/dist/cjs/createPermissions.native.js +6 -7
- package/dist/cjs/createPermissions.native.js.map +1 -1
- package/dist/cjs/createZeroClient.cjs +1 -1
- package/dist/cjs/createZeroClient.native.js +1 -1
- package/dist/cjs/createZeroClient.native.js.map +1 -1
- package/dist/cjs/helpers/asyncContext.cjs +5 -48
- package/dist/cjs/helpers/asyncContext.native.js +3 -48
- package/dist/cjs/helpers/asyncContext.native.js.map +1 -1
- package/dist/cjs/helpers/createMutators.cjs +6 -5
- package/dist/cjs/helpers/createMutators.native.js +6 -5
- package/dist/cjs/helpers/createMutators.native.js.map +1 -1
- package/dist/cjs/helpers/createMutators.test.cjs +45 -3
- package/dist/cjs/helpers/createMutators.test.native.js +59 -3
- package/dist/cjs/helpers/createMutators.test.native.js.map +1 -1
- package/dist/cjs/helpers/ensureLoggedIn.cjs +2 -4
- package/dist/cjs/helpers/ensureLoggedIn.native.js +2 -4
- package/dist/cjs/helpers/ensureLoggedIn.native.js.map +1 -1
- package/dist/cjs/helpers/mutatorContext.test.cjs +105 -0
- package/dist/cjs/helpers/mutatorContext.test.native.js +120 -0
- package/dist/cjs/helpers/mutatorContext.test.native.js.map +1 -0
- package/dist/cjs/server.cjs +1 -1
- package/dist/cjs/server.native.js +1 -1
- package/dist/cjs/server.native.js.map +1 -1
- package/dist/cjs/zeroRunner.native.js.map +1 -1
- package/dist/esm/createPermissions.mjs +7 -10
- package/dist/esm/createPermissions.mjs.map +1 -1
- package/dist/esm/createPermissions.native.js +6 -7
- package/dist/esm/createPermissions.native.js.map +1 -1
- package/dist/esm/createZeroClient.mjs +1 -1
- package/dist/esm/createZeroClient.mjs.map +1 -1
- package/dist/esm/createZeroClient.native.js +1 -1
- package/dist/esm/createZeroClient.native.js.map +1 -1
- package/dist/esm/helpers/asyncContext.mjs +5 -48
- package/dist/esm/helpers/asyncContext.mjs.map +1 -1
- package/dist/esm/helpers/asyncContext.native.js +3 -48
- package/dist/esm/helpers/asyncContext.native.js.map +1 -1
- package/dist/esm/helpers/createMutators.mjs +6 -5
- package/dist/esm/helpers/createMutators.mjs.map +1 -1
- package/dist/esm/helpers/createMutators.native.js +6 -5
- package/dist/esm/helpers/createMutators.native.js.map +1 -1
- package/dist/esm/helpers/createMutators.test.mjs +45 -3
- package/dist/esm/helpers/createMutators.test.mjs.map +1 -1
- package/dist/esm/helpers/createMutators.test.native.js +59 -3
- package/dist/esm/helpers/createMutators.test.native.js.map +1 -1
- package/dist/esm/helpers/ensureLoggedIn.mjs +2 -4
- package/dist/esm/helpers/ensureLoggedIn.mjs.map +1 -1
- package/dist/esm/helpers/ensureLoggedIn.native.js +2 -4
- package/dist/esm/helpers/ensureLoggedIn.native.js.map +1 -1
- package/dist/esm/helpers/mutatorContext.test.mjs +82 -0
- package/dist/esm/helpers/mutatorContext.test.mjs.map +1 -0
- package/dist/esm/helpers/mutatorContext.test.native.js +94 -0
- package/dist/esm/helpers/mutatorContext.test.native.js.map +1 -0
- package/dist/esm/server.mjs +1 -1
- package/dist/esm/server.mjs.map +1 -1
- package/dist/esm/server.native.js +1 -1
- package/dist/esm/server.native.js.map +1 -1
- package/dist/esm/zeroRunner.mjs.map +1 -1
- package/dist/esm/zeroRunner.native.js.map +1 -1
- package/package.json +2 -2
- package/src/createPermissions.ts +11 -8
- package/src/createZeroClient.tsx +1 -1
- package/src/helpers/asyncContext.native.ts +10 -76
- package/src/helpers/asyncContext.ts +18 -79
- package/src/helpers/createMutators.test.ts +49 -3
- package/src/helpers/createMutators.ts +11 -7
- package/src/helpers/ensureLoggedIn.ts +7 -2
- package/src/helpers/mutatorContext.test.ts +96 -0
- package/src/server.ts +1 -1
- package/src/zeroRunner.ts +6 -0
- package/types/createPermissions.d.ts +2 -2
- package/types/createPermissions.d.ts.map +1 -1
- package/types/helpers/asyncContext.d.ts.map +1 -1
- package/types/helpers/asyncContext.native.d.ts.map +1 -1
- package/types/helpers/createMutators.d.ts +3 -3
- package/types/helpers/createMutators.d.ts.map +1 -1
- package/types/helpers/ensureLoggedIn.d.ts.map +1 -1
- package/types/helpers/mutatorContext.test.d.ts +2 -0
- package/types/helpers/mutatorContext.test.d.ts.map +1 -0
- package/types/zeroRunner.d.ts.map +1 -1
package/src/createPermissions.ts
CHANGED
|
@@ -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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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(
|
|
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
|
-
|
|
154
|
+
bindCan,
|
|
152
155
|
buildPermissionQuery,
|
|
153
156
|
}
|
|
154
157
|
}
|
package/src/createZeroClient.tsx
CHANGED
|
@@ -5,86 +5,20 @@ interface AsyncContext<T> {
|
|
|
5
5
|
|
|
6
6
|
export function setupAsyncLocalStorage(_AsyncLocalStorage: unknown): void {}
|
|
7
7
|
|
|
8
|
-
// react native
|
|
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
|
|
18
|
+
return undefined
|
|
16
19
|
},
|
|
17
|
-
async run<R>(
|
|
18
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
|
92
|
+
return undefined
|
|
85
93
|
},
|
|
86
|
-
async run<R>(
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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":"
|
|
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,
|
|
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;
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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;
|
|
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 @@
|
|
|
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,
|
|
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"}
|