over-zero 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/build/readPermissions.cjs +51 -0
- package/dist/cjs/build/readPermissions.js +48 -0
- package/dist/cjs/build/readPermissions.js.map +6 -0
- package/dist/cjs/build/readPermissions.native.js +56 -0
- package/dist/cjs/build/readPermissions.native.js.map +6 -0
- package/dist/cjs/build/schema.cjs +28 -0
- package/dist/cjs/build/schema.js +22 -0
- package/dist/cjs/build/schema.js.map +6 -0
- package/dist/cjs/build/schema.native.js +28 -0
- package/dist/cjs/build/schema.native.js.map +6 -0
- package/dist/cjs/createClient.cjs +89 -0
- package/dist/cjs/createClient.js +76 -0
- package/dist/cjs/createClient.js.map +6 -0
- package/dist/cjs/createClient.native.js +81 -0
- package/dist/cjs/createClient.native.js.map +6 -0
- package/dist/cjs/createMutations.cjs +50 -0
- package/dist/cjs/createMutations.js +43 -0
- package/dist/cjs/createMutations.js.map +6 -0
- package/dist/cjs/createMutations.native.js +50 -0
- package/dist/cjs/createMutations.native.js.map +6 -0
- package/dist/cjs/createPermissions.cjs +128 -0
- package/dist/cjs/createPermissions.js +121 -0
- package/dist/cjs/createPermissions.js.map +6 -0
- package/dist/cjs/createPermissions.native.js +135 -0
- package/dist/cjs/createPermissions.native.js.map +6 -0
- package/dist/cjs/createServer.cjs +92 -0
- package/dist/cjs/createServer.js +71 -0
- package/dist/cjs/createServer.js.map +6 -0
- package/dist/cjs/createServer.native.js +75 -0
- package/dist/cjs/createServer.native.js.map +6 -0
- package/dist/cjs/helpers/batchQuery.cjs +49 -0
- package/dist/cjs/helpers/batchQuery.js +38 -0
- package/dist/cjs/helpers/batchQuery.js.map +6 -0
- package/dist/cjs/helpers/batchQuery.native.js +42 -0
- package/dist/cjs/helpers/batchQuery.native.js.map +6 -0
- package/dist/cjs/helpers/clearZeroDatabase.cjs +57 -0
- package/dist/cjs/helpers/clearZeroDatabase.js +57 -0
- package/dist/cjs/helpers/clearZeroDatabase.js.map +6 -0
- package/dist/cjs/helpers/clearZeroDatabase.native.js +71 -0
- package/dist/cjs/helpers/clearZeroDatabase.native.js.map +6 -0
- package/dist/cjs/helpers/context.cjs +40 -0
- package/dist/cjs/helpers/context.js +36 -0
- package/dist/cjs/helpers/context.js.map +6 -0
- package/dist/cjs/helpers/context.native.js +42 -0
- package/dist/cjs/helpers/context.native.js.map +6 -0
- package/dist/cjs/helpers/createMutators.cjs +87 -0
- package/dist/cjs/helpers/createMutators.js +81 -0
- package/dist/cjs/helpers/createMutators.js.map +6 -0
- package/dist/cjs/helpers/createMutators.native.js +116 -0
- package/dist/cjs/helpers/createMutators.native.js.map +6 -0
- package/dist/cjs/helpers/ensureLoggedIn.cjs +33 -0
- package/dist/cjs/helpers/ensureLoggedIn.js +25 -0
- package/dist/cjs/helpers/ensureLoggedIn.js.map +6 -0
- package/dist/cjs/helpers/ensureLoggedIn.native.js +29 -0
- package/dist/cjs/helpers/ensureLoggedIn.native.js.map +6 -0
- package/dist/cjs/helpers/getAuthData.cjs +36 -0
- package/dist/cjs/helpers/getAuthData.js +29 -0
- package/dist/cjs/helpers/getAuthData.js.map +6 -0
- package/dist/cjs/helpers/getAuthData.native.js +33 -0
- package/dist/cjs/helpers/getAuthData.native.js.map +6 -0
- package/dist/cjs/helpers/prettyFormatZeroQuery.cjs +107 -0
- package/dist/cjs/helpers/prettyFormatZeroQuery.js +92 -0
- package/dist/cjs/helpers/prettyFormatZeroQuery.js.map +6 -0
- package/dist/cjs/helpers/prettyFormatZeroQuery.native.js +99 -0
- package/dist/cjs/helpers/prettyFormatZeroQuery.native.js.map +6 -0
- package/dist/cjs/helpers/setupZeroClientGlobalEffects.cjs +40 -0
- package/dist/cjs/helpers/setupZeroClientGlobalEffects.js +36 -0
- package/dist/cjs/helpers/setupZeroClientGlobalEffects.js.map +6 -0
- package/dist/cjs/helpers/setupZeroClientGlobalEffects.native.js +36 -0
- package/dist/cjs/helpers/setupZeroClientGlobalEffects.native.js.map +6 -0
- package/dist/cjs/helpers/useAuthData.cjs +32 -0
- package/dist/cjs/helpers/useAuthData.js +25 -0
- package/dist/cjs/helpers/useAuthData.js.map +6 -0
- package/dist/cjs/helpers/useAuthData.native.js +33 -0
- package/dist/cjs/helpers/useAuthData.native.js.map +6 -0
- package/dist/cjs/helpers/useZDB.cjs +70 -0
- package/dist/cjs/helpers/useZDB.js +51 -0
- package/dist/cjs/helpers/useZDB.js.map +6 -0
- package/dist/cjs/helpers/useZDB.native.js +68 -0
- package/dist/cjs/helpers/useZDB.native.js.map +6 -0
- package/dist/cjs/helpers/zeroEmitter.cjs +27 -0
- package/dist/cjs/helpers/zeroEmitter.js +22 -0
- package/dist/cjs/helpers/zeroEmitter.js.map +6 -0
- package/dist/cjs/helpers/zeroEmitter.native.js +26 -0
- package/dist/cjs/helpers/zeroEmitter.native.js.map +6 -0
- package/dist/cjs/index.cjs +23 -0
- package/dist/cjs/index.js +20 -0
- package/dist/cjs/index.js.map +6 -0
- package/dist/cjs/index.native.js +30 -0
- package/dist/cjs/index.native.js.map +6 -0
- package/dist/cjs/types.cjs +16 -0
- package/dist/cjs/types.js +14 -0
- package/dist/cjs/types.js.map +6 -0
- package/dist/cjs/types.native.js +15 -0
- package/dist/cjs/types.native.js.map +6 -0
- package/dist/esm/build/readPermissions.js +36 -0
- package/dist/esm/build/readPermissions.js.map +6 -0
- package/dist/esm/build/readPermissions.mjs +28 -0
- package/dist/esm/build/readPermissions.mjs.map +1 -0
- package/dist/esm/build/readPermissions.native.js +34 -0
- package/dist/esm/build/readPermissions.native.js.map +1 -0
- package/dist/esm/build/schema.js +7 -0
- package/dist/esm/build/schema.js.map +6 -0
- package/dist/esm/build/schema.mjs +4 -0
- package/dist/esm/build/schema.mjs.map +1 -0
- package/dist/esm/build/schema.native.js +4 -0
- package/dist/esm/build/schema.native.js.map +1 -0
- package/dist/esm/createClient.js +68 -0
- package/dist/esm/createClient.js.map +6 -0
- package/dist/esm/createClient.mjs +66 -0
- package/dist/esm/createClient.mjs.map +1 -0
- package/dist/esm/createClient.native.js +74 -0
- package/dist/esm/createClient.native.js.map +1 -0
- package/dist/esm/createMutations.js +27 -0
- package/dist/esm/createMutations.js.map +6 -0
- package/dist/esm/createMutations.mjs +27 -0
- package/dist/esm/createMutations.mjs.map +1 -0
- package/dist/esm/createMutations.native.js +29 -0
- package/dist/esm/createMutations.native.js.map +1 -0
- package/dist/esm/createPermissions.js +106 -0
- package/dist/esm/createPermissions.js.map +6 -0
- package/dist/esm/createPermissions.mjs +105 -0
- package/dist/esm/createPermissions.mjs.map +1 -0
- package/dist/esm/createPermissions.native.js +129 -0
- package/dist/esm/createPermissions.native.js.map +1 -0
- package/dist/esm/createServer.js +54 -0
- package/dist/esm/createServer.js.map +6 -0
- package/dist/esm/createServer.mjs +58 -0
- package/dist/esm/createServer.mjs.map +1 -0
- package/dist/esm/createServer.native.js +61 -0
- package/dist/esm/createServer.native.js.map +1 -0
- package/dist/esm/helpers/batchQuery.js +22 -0
- package/dist/esm/helpers/batchQuery.js.map +6 -0
- package/dist/esm/helpers/batchQuery.mjs +26 -0
- package/dist/esm/helpers/batchQuery.mjs.map +1 -0
- package/dist/esm/helpers/batchQuery.native.js +23 -0
- package/dist/esm/helpers/batchQuery.native.js.map +1 -0
- package/dist/esm/helpers/clearZeroDatabase.js +42 -0
- package/dist/esm/helpers/clearZeroDatabase.js.map +6 -0
- package/dist/esm/helpers/clearZeroDatabase.mjs +34 -0
- package/dist/esm/helpers/clearZeroDatabase.mjs.map +1 -0
- package/dist/esm/helpers/clearZeroDatabase.native.js +50 -0
- package/dist/esm/helpers/clearZeroDatabase.native.js.map +1 -0
- package/dist/esm/helpers/context.js +20 -0
- package/dist/esm/helpers/context.js.map +6 -0
- package/dist/esm/helpers/context.mjs +15 -0
- package/dist/esm/helpers/context.mjs.map +1 -0
- package/dist/esm/helpers/context.native.js +15 -0
- package/dist/esm/helpers/context.native.js.map +1 -0
- package/dist/esm/helpers/createMutators.js +69 -0
- package/dist/esm/helpers/createMutators.js.map +6 -0
- package/dist/esm/helpers/createMutators.mjs +64 -0
- package/dist/esm/helpers/createMutators.mjs.map +1 -0
- package/dist/esm/helpers/createMutators.native.js +101 -0
- package/dist/esm/helpers/createMutators.native.js.map +1 -0
- package/dist/esm/helpers/ensureLoggedIn.js +10 -0
- package/dist/esm/helpers/ensureLoggedIn.js.map +6 -0
- package/dist/esm/helpers/ensureLoggedIn.mjs +10 -0
- package/dist/esm/helpers/ensureLoggedIn.mjs.map +1 -0
- package/dist/esm/helpers/ensureLoggedIn.native.js +10 -0
- package/dist/esm/helpers/ensureLoggedIn.native.js.map +1 -0
- package/dist/esm/helpers/getAuthData.js +13 -0
- package/dist/esm/helpers/getAuthData.js.map +6 -0
- package/dist/esm/helpers/getAuthData.mjs +13 -0
- package/dist/esm/helpers/getAuthData.mjs.map +1 -0
- package/dist/esm/helpers/getAuthData.native.js +13 -0
- package/dist/esm/helpers/getAuthData.native.js.map +1 -0
- package/dist/esm/helpers/prettyFormatZeroQuery.js +76 -0
- package/dist/esm/helpers/prettyFormatZeroQuery.js.map +6 -0
- package/dist/esm/helpers/prettyFormatZeroQuery.mjs +84 -0
- package/dist/esm/helpers/prettyFormatZeroQuery.mjs.map +1 -0
- package/dist/esm/helpers/prettyFormatZeroQuery.native.js +93 -0
- package/dist/esm/helpers/prettyFormatZeroQuery.native.js.map +1 -0
- package/dist/esm/helpers/setupZeroClientGlobalEffects.js +40 -0
- package/dist/esm/helpers/setupZeroClientGlobalEffects.js.map +6 -0
- package/dist/esm/helpers/setupZeroClientGlobalEffects.mjs +41 -0
- package/dist/esm/helpers/setupZeroClientGlobalEffects.mjs.map +1 -0
- package/dist/esm/helpers/setupZeroClientGlobalEffects.native.js +41 -0
- package/dist/esm/helpers/setupZeroClientGlobalEffects.native.js.map +1 -0
- package/dist/esm/helpers/useAuthData.js +11 -0
- package/dist/esm/helpers/useAuthData.js.map +6 -0
- package/dist/esm/helpers/useAuthData.mjs +9 -0
- package/dist/esm/helpers/useAuthData.mjs.map +1 -0
- package/dist/esm/helpers/useAuthData.native.js +13 -0
- package/dist/esm/helpers/useAuthData.native.js.map +1 -0
- package/dist/esm/helpers/useZDB.js +38 -0
- package/dist/esm/helpers/useZDB.js.map +6 -0
- package/dist/esm/helpers/useZDB.mjs +47 -0
- package/dist/esm/helpers/useZDB.mjs.map +1 -0
- package/dist/esm/helpers/useZDB.native.js +55 -0
- package/dist/esm/helpers/useZDB.native.js.map +1 -0
- package/dist/esm/helpers/zeroEmitter.js +6 -0
- package/dist/esm/helpers/zeroEmitter.js.map +6 -0
- package/dist/esm/helpers/zeroEmitter.mjs +4 -0
- package/dist/esm/helpers/zeroEmitter.mjs.map +1 -0
- package/dist/esm/helpers/zeroEmitter.native.js +4 -0
- package/dist/esm/helpers/zeroEmitter.native.js.map +1 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +6 -0
- package/dist/esm/index.mjs +7 -0
- package/dist/esm/index.mjs.map +1 -0
- package/dist/esm/index.native.js +7 -0
- package/dist/esm/index.native.js.map +1 -0
- package/dist/esm/types.js +1 -0
- package/dist/esm/types.js.map +6 -0
- package/dist/esm/types.mjs +2 -0
- package/dist/esm/types.mjs.map +1 -0
- package/dist/esm/types.native.js +2 -0
- package/dist/esm/types.native.js.map +1 -0
- package/package.json +51 -0
- package/readme.md +16 -0
- package/src/createPermissions.ts +281 -0
- package/src/createZeroClient.tsx +191 -0
- package/src/createZeroServer.ts +153 -0
- package/src/helpers/batchQuery.ts +45 -0
- package/src/helpers/clearZeroDatabase.ts +68 -0
- package/src/helpers/context.ts +28 -0
- package/src/helpers/createMutators.ts +139 -0
- package/src/helpers/ensureLoggedIn.ts +8 -0
- package/src/helpers/getAuthData.tsx +12 -0
- package/src/helpers/prettyFormatZeroQuery.ts +167 -0
- package/src/helpers/useAuthData.ts +13 -0
- package/src/helpers/useZeroDebug.ts +104 -0
- package/src/helpers/zeroEmitter.ts +5 -0
- package/src/index.ts +15 -0
- package/src/mutations.ts +121 -0
- package/src/types.ts +49 -0
- package/types/createMutations.d.ts +20 -0
- package/types/createMutations.d.ts.map +1 -0
- package/types/createPermissions.d.ts +37 -0
- package/types/createPermissions.d.ts.map +1 -0
- package/types/createZeroClient.d.ts +45 -0
- package/types/createZeroClient.d.ts.map +1 -0
- package/types/createZeroServer.d.ts +61 -0
- package/types/createZeroServer.d.ts.map +1 -0
- package/types/helpers/batchQuery.d.ts +7 -0
- package/types/helpers/batchQuery.d.ts.map +1 -0
- package/types/helpers/clearZeroDatabase.d.ts +2 -0
- package/types/helpers/clearZeroDatabase.d.ts.map +1 -0
- package/types/helpers/context.d.ts +5 -0
- package/types/helpers/context.d.ts.map +1 -0
- package/types/helpers/createMutators.d.ts +16 -0
- package/types/helpers/createMutators.d.ts.map +1 -0
- package/types/helpers/ensureLoggedIn.d.ts +2 -0
- package/types/helpers/ensureLoggedIn.d.ts.map +1 -0
- package/types/helpers/getAuthData.d.ts +1 -0
- package/types/helpers/getAuthData.d.ts.map +1 -0
- package/types/helpers/prettyFormatZeroQuery.d.ts +3 -0
- package/types/helpers/prettyFormatZeroQuery.d.ts.map +1 -0
- package/types/helpers/useAuthData.d.ts +1 -0
- package/types/helpers/useAuthData.d.ts.map +1 -0
- package/types/helpers/useZeroDebug.d.ts +3 -0
- package/types/helpers/useZeroDebug.d.ts.map +1 -0
- package/types/helpers/zeroEmitter.d.ts +2 -0
- package/types/helpers/zeroEmitter.d.ts.map +1 -0
- package/types/index.d.ts +9 -0
- package/types/index.d.ts.map +1 -0
- package/types/types.d.ts +21 -0
- package/types/types.d.ts.map +1 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Condition,
|
|
3
|
+
ExpressionBuilder,
|
|
4
|
+
Query,
|
|
5
|
+
Transaction,
|
|
6
|
+
Schema as ZeroSchema,
|
|
7
|
+
} from '@rocicorp/zero'
|
|
8
|
+
import { ANYONE_CAN, definePermissions } from '@rocicorp/zero'
|
|
9
|
+
import { ensure, EnsureError, objectEntries } from '@vxrn/helpers'
|
|
10
|
+
import type { AuthData } from 'start/types'
|
|
11
|
+
// import { models } from '~/data/models'
|
|
12
|
+
// import { objectEntries } from '~/helpers/types/object'
|
|
13
|
+
import { prettyFormatZeroQuery } from './helpers/prettyFormatZeroQuery'
|
|
14
|
+
import type { MutatorContext, Where } from './types'
|
|
15
|
+
|
|
16
|
+
// TODO this will go away soon if we can get a good sync queries setup
|
|
17
|
+
|
|
18
|
+
export function createPermissions<Schema extends ZeroSchema, TableName extends string>({
|
|
19
|
+
schema,
|
|
20
|
+
getContext,
|
|
21
|
+
}: {
|
|
22
|
+
schema: Schema
|
|
23
|
+
getContext: () => MutatorContext
|
|
24
|
+
}) {
|
|
25
|
+
runEnvironmentSafetyCheck()
|
|
26
|
+
|
|
27
|
+
// we don't want flickers as you move around and these queries are re-run
|
|
28
|
+
// and things generally aren't changing with permissions rapidly, so lets
|
|
29
|
+
// cache the last results and use that when first rendering, they will
|
|
30
|
+
// always update once the query resolves
|
|
31
|
+
|
|
32
|
+
// Where defaults to letting you return basically anything because we want to allow it to be usable
|
|
33
|
+
// flexibly, one example is for permissions where you can return an Object with multiple wheres as values
|
|
34
|
+
// for example, PermissionsWhere below
|
|
35
|
+
|
|
36
|
+
function where<Table extends TableName, Builder extends Where = Where<Table>>(
|
|
37
|
+
tableName: Table,
|
|
38
|
+
builder: Builder
|
|
39
|
+
): Builder
|
|
40
|
+
|
|
41
|
+
function where<Table extends TableName, Builder extends Where = Where<Table>>(
|
|
42
|
+
builder: Builder
|
|
43
|
+
): Builder
|
|
44
|
+
|
|
45
|
+
function where<Table extends TableName, Builder extends Where = Where<Table>>(
|
|
46
|
+
a: Table | Builder,
|
|
47
|
+
b?: Builder
|
|
48
|
+
): Builder {
|
|
49
|
+
if (b) {
|
|
50
|
+
WhereTableNameMap.set(b, a as Table)
|
|
51
|
+
}
|
|
52
|
+
return (b || a) as any
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// permissions where:
|
|
56
|
+
|
|
57
|
+
const WhereTableNameMap = new WeakMap<Where, TableName>()
|
|
58
|
+
|
|
59
|
+
function getWhereTableName(where: Where) {
|
|
60
|
+
return WhereTableNameMap.get(where)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// example of a custom where we use for permissions
|
|
64
|
+
|
|
65
|
+
type PermissionPresetActions =
|
|
66
|
+
| 'read'
|
|
67
|
+
| 'write'
|
|
68
|
+
| 'insert'
|
|
69
|
+
| 'update'
|
|
70
|
+
| 'delete'
|
|
71
|
+
| 'select'
|
|
72
|
+
|
|
73
|
+
type PermissionsConditions = Partial<
|
|
74
|
+
Record<PermissionPresetActions | (string & {}), Condition | boolean>
|
|
75
|
+
>
|
|
76
|
+
|
|
77
|
+
type PermissionsWhere<Table extends TableName = TableName> = Where<
|
|
78
|
+
Table,
|
|
79
|
+
PermissionsConditions
|
|
80
|
+
>
|
|
81
|
+
|
|
82
|
+
const fallbackActions: Record<string, string> = {
|
|
83
|
+
select: 'read',
|
|
84
|
+
insert: 'write',
|
|
85
|
+
update: 'write',
|
|
86
|
+
upsert: 'write',
|
|
87
|
+
delete: 'write',
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function buildPermissionQuery<
|
|
91
|
+
PermissionWhere extends PermissionsWhere,
|
|
92
|
+
Action extends string,
|
|
93
|
+
>(
|
|
94
|
+
authData: AuthData | null,
|
|
95
|
+
eb: ExpressionBuilder<any, any>,
|
|
96
|
+
permissionWhere: PermissionWhere,
|
|
97
|
+
action: Action,
|
|
98
|
+
// TODO until i can get a working PickPrimaryKeys<'message'>
|
|
99
|
+
objOrId: Record<string, any> | string
|
|
100
|
+
) {
|
|
101
|
+
const tableName = getWhereTableName(permissionWhere)
|
|
102
|
+
|
|
103
|
+
if (!tableName) {
|
|
104
|
+
throw new Error(`Must use PermissionWhere for buildPermissionQuery`)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const tableSchema = schema.tables[tableName]
|
|
108
|
+
const primaryKeys = tableSchema.primaryKey
|
|
109
|
+
const permissionQueryBuilder = permissionWhere(eb, authData)
|
|
110
|
+
const fallbackAction = fallbackActions[action]
|
|
111
|
+
|
|
112
|
+
const permissionCondition =
|
|
113
|
+
permissionQueryBuilder[action] ||
|
|
114
|
+
(fallbackAction ? permissionQueryBuilder[fallbackAction] : undefined)
|
|
115
|
+
|
|
116
|
+
if (permissionCondition == null) {
|
|
117
|
+
throw new Error(`No permission defined for ${action} (or ${fallbackAction})`)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (permissionCondition === true) {
|
|
121
|
+
return eb.cmpLit(true, '=', true)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (permissionCondition === false) {
|
|
125
|
+
return eb.cmpLit(true, '=', false)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const primaryKeyWheres: Condition[] = []
|
|
129
|
+
|
|
130
|
+
for (const key of primaryKeys) {
|
|
131
|
+
const value = typeof objOrId === 'string' ? objOrId : objOrId[key]
|
|
132
|
+
primaryKeyWheres.push(eb.cmp(key as any, value))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return eb.and(permissionCondition, ...primaryKeyWheres)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function can<
|
|
139
|
+
PWhere extends PermissionsWhere,
|
|
140
|
+
Action extends keyof ReturnType<PWhere>,
|
|
141
|
+
>(where: PWhere, action: Action, obj: any) {
|
|
142
|
+
const ctx = getContext()
|
|
143
|
+
const tableName = getWhereTableName(where)
|
|
144
|
+
if (!tableName) {
|
|
145
|
+
throw new Error(`Must use where('table') style where to pass to can()`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// on client we always allow!
|
|
149
|
+
if (process.env.VITE_ENVIRONMENT === 'ssr') {
|
|
150
|
+
await ensurePermission(
|
|
151
|
+
ctx.tx,
|
|
152
|
+
ctx.authData,
|
|
153
|
+
tableName,
|
|
154
|
+
where,
|
|
155
|
+
action as string,
|
|
156
|
+
obj
|
|
157
|
+
)
|
|
158
|
+
ctx.didCanPermissionsRun = true
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
type TX = Transaction<Schema>
|
|
163
|
+
|
|
164
|
+
async function ensurePermission<
|
|
165
|
+
PW extends PermissionsWhere,
|
|
166
|
+
Action extends keyof ReturnType<PW>,
|
|
167
|
+
>(
|
|
168
|
+
tx: TX,
|
|
169
|
+
authData: AuthData | null,
|
|
170
|
+
tableName: TableName,
|
|
171
|
+
where: Where,
|
|
172
|
+
actionIn: Action,
|
|
173
|
+
obj: any // TODO until i can get a working PickPrimaryKeys<'message'>
|
|
174
|
+
): Promise<void> {
|
|
175
|
+
if (authData?.role === 'admin') {
|
|
176
|
+
// admin role can do any mutation
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const action = String(actionIn)
|
|
181
|
+
const name = `${tableName}.${action}`
|
|
182
|
+
const queryBase = tx.query[tableName] as Query<any, any>
|
|
183
|
+
let query: Query<any, any, any> | null = null
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
query = queryBase
|
|
187
|
+
.where((eb) => {
|
|
188
|
+
return buildPermissionQuery(authData, eb, where, action, obj)
|
|
189
|
+
})
|
|
190
|
+
.one()
|
|
191
|
+
|
|
192
|
+
ensure(await query)
|
|
193
|
+
} catch (err) {
|
|
194
|
+
const errorTitle = `${name} with auth id: ${authData?.id}`
|
|
195
|
+
|
|
196
|
+
if (err instanceof EnsureError) {
|
|
197
|
+
let msg = `[permission] 🚫 Not Allowed: ${errorTitle}`
|
|
198
|
+
if (process.env.NODE_ENV === 'development' && query) {
|
|
199
|
+
msg += `\n ${prettyFormatZeroQuery(query)}`
|
|
200
|
+
}
|
|
201
|
+
throw new Error(msg)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
throw new Error(`Error running permission ${errorTitle}\n${err}`)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const readPermissions = definePermissions<AuthData, Schema>(schema, async () => {
|
|
209
|
+
const permissionsEntries = await Promise.all(
|
|
210
|
+
objectEntries(models).map(async ([key, model]) => {
|
|
211
|
+
return await runWithContext(
|
|
212
|
+
{
|
|
213
|
+
authData: { id: '', role: undefined, email: '' },
|
|
214
|
+
} as any,
|
|
215
|
+
() => {
|
|
216
|
+
return [
|
|
217
|
+
key,
|
|
218
|
+
{
|
|
219
|
+
row: {
|
|
220
|
+
select: [
|
|
221
|
+
(authData: AuthData, eb: ExpressionBuilder<any, any>) => {
|
|
222
|
+
const out = model.permissions(eb, authData).read
|
|
223
|
+
|
|
224
|
+
if (out === true) {
|
|
225
|
+
return eb.and()
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (out === false) {
|
|
229
|
+
return eb.cmpLit(true, '=', false)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return out
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
// we have permissions on these through our model system with custom mutators:
|
|
236
|
+
insert: ANYONE_CAN,
|
|
237
|
+
update: ANYONE_CAN,
|
|
238
|
+
delete: ANYONE_CAN,
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
]
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
})
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
const permissions = Object.fromEntries(permissionsEntries)
|
|
248
|
+
|
|
249
|
+
return permissions as any
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
where,
|
|
254
|
+
can,
|
|
255
|
+
buildPermissionQuery,
|
|
256
|
+
readPermissions,
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// this is just so we have some assurance that we aren't skipping permissions
|
|
261
|
+
// checks due to bad VITE_ENVIRONMENT variable
|
|
262
|
+
function runEnvironmentSafetyCheck() {
|
|
263
|
+
if (typeof document !== 'undefined') {
|
|
264
|
+
// web!
|
|
265
|
+
} else if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
|
|
266
|
+
// react-native!
|
|
267
|
+
} else {
|
|
268
|
+
// server!
|
|
269
|
+
if (process.env.VITE_ENVIRONMENT !== 'ssr') {
|
|
270
|
+
console.error(`❌❌❌❌
|
|
271
|
+
|
|
272
|
+
ERROR: VITE_ENVIRONMENT is not set to "ssr" on server, which means permissions checks won't run when they should
|
|
273
|
+
This is makes Zero entirely insecure and needs to be fixed immediately.
|
|
274
|
+
|
|
275
|
+
This is likely a One framework issue, unless the user Vite config is overwriting the value.
|
|
276
|
+
One automatically sets this value.
|
|
277
|
+
|
|
278
|
+
`)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { Row, ZeroOptions, Schema as ZeroSchema } from '@rocicorp/zero'
|
|
2
|
+
import { useZero, ZeroProvider, useQuery as zeroUseQuery } from '@rocicorp/zero/react'
|
|
3
|
+
import { createLocalStorage, mapObject } from '@vxrn/helpers'
|
|
4
|
+
import { createContext, use, useMemo, type ReactNode } from 'react'
|
|
5
|
+
import { createPermissions } from './createPermissions'
|
|
6
|
+
import { context } from './helpers/context'
|
|
7
|
+
import { createMutators } from './helpers/createMutators'
|
|
8
|
+
import { prettyFormatZeroQuery } from './helpers/prettyFormatZeroQuery'
|
|
9
|
+
import { useZeroDebug } from './helpers/useZeroDebug'
|
|
10
|
+
import { zeroEmitter } from './helpers/zeroEmitter'
|
|
11
|
+
import type { AuthData } from './types'
|
|
12
|
+
|
|
13
|
+
export function createZero<Schema extends ZeroSchema>({
|
|
14
|
+
schema,
|
|
15
|
+
models,
|
|
16
|
+
disable,
|
|
17
|
+
}: {
|
|
18
|
+
schema: Schema
|
|
19
|
+
models: any
|
|
20
|
+
disable?: boolean
|
|
21
|
+
}) {
|
|
22
|
+
// TODO
|
|
23
|
+
type TableName = string
|
|
24
|
+
type Mutators = any // typeof mutators
|
|
25
|
+
type ZeroInstance = ReturnType<typeof useZero<Schema, Mutators>>
|
|
26
|
+
|
|
27
|
+
const modelPermissions = mapObject(models, (val) => val.permissions) as {
|
|
28
|
+
[K in keyof typeof models]: (typeof models)[K]['permissions']
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const permissionsHelpers = createPermissions<Schema, TableName>({
|
|
32
|
+
schema,
|
|
33
|
+
getContext: context,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const permissionCache = createLocalStorage<string, boolean>('permissions-cache', {
|
|
37
|
+
storageLimit: 24,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const AuthDataContext = createContext<AuthData>(null)
|
|
41
|
+
const useAuthData = () => use(AuthDataContext)
|
|
42
|
+
|
|
43
|
+
function usePermission<
|
|
44
|
+
K extends TableName,
|
|
45
|
+
Action extends 'insert' | 'update' | 'delete' | 'select',
|
|
46
|
+
>(
|
|
47
|
+
table: K,
|
|
48
|
+
action: Action,
|
|
49
|
+
objOrId: string | Partial<Row<Schema['tables'][K]>> | undefined,
|
|
50
|
+
enabled = typeof objOrId !== 'undefined',
|
|
51
|
+
debug = false
|
|
52
|
+
): boolean | null {
|
|
53
|
+
// we fallback to just table.action, to avoid flickers for now
|
|
54
|
+
const keyBase = `${table}${action}`
|
|
55
|
+
const key = `${keyBase}${typeof objOrId === 'string' ? objOrId : JSON.stringify(objOrId)}`
|
|
56
|
+
const cacheVal = permissionCache.get(key) ?? permissionCache.get(keyBase)
|
|
57
|
+
const authData = useAuthData()
|
|
58
|
+
const permission = modelPermissions[table]
|
|
59
|
+
|
|
60
|
+
const query = (() => {
|
|
61
|
+
let baseQuery = zero.query[table].one()
|
|
62
|
+
|
|
63
|
+
if (!enabled) {
|
|
64
|
+
return baseQuery
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return baseQuery.where((eb) => {
|
|
68
|
+
return permissionsHelpers.buildPermissionQuery(
|
|
69
|
+
authData,
|
|
70
|
+
eb,
|
|
71
|
+
permission,
|
|
72
|
+
action,
|
|
73
|
+
objOrId as any
|
|
74
|
+
)
|
|
75
|
+
})
|
|
76
|
+
})()
|
|
77
|
+
|
|
78
|
+
const [data, status] = useQuery(query, {
|
|
79
|
+
enabled: Boolean(enabled && authData && objOrId),
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
if (debug) {
|
|
83
|
+
console.info(
|
|
84
|
+
`usePermission()`,
|
|
85
|
+
{ data, status, action, authData, permission },
|
|
86
|
+
prettyFormatZeroQuery(query)
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const result = data
|
|
91
|
+
|
|
92
|
+
const allowed = Boolean(result)
|
|
93
|
+
|
|
94
|
+
if (!objOrId) {
|
|
95
|
+
return false
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return allowed
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let latestZeroInstance: ZeroInstance | null = null
|
|
102
|
+
|
|
103
|
+
// when we log in this is swapped out and later the exported zero is just a proxy
|
|
104
|
+
// may cause some issues, ideally zero itself supports this
|
|
105
|
+
const zero: ZeroInstance = new Proxy({} as never, {
|
|
106
|
+
get(_, key) {
|
|
107
|
+
return Reflect.get(latestZeroInstance!, key, latestZeroInstance)
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const useQuery: typeof zeroUseQuery = (query, options) => {
|
|
112
|
+
if (disable) {
|
|
113
|
+
return [null, { type: 'unknown' }] as never
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const out = zeroUseQuery(query, options)
|
|
117
|
+
|
|
118
|
+
if (process.env.NODE_ENV === 'development') {
|
|
119
|
+
// biome-ignore lint/correctness/useHookAtTopLevel: ok
|
|
120
|
+
useZeroDebug(query, options, out)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return out
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const ProvideZero = ({
|
|
127
|
+
children,
|
|
128
|
+
authData,
|
|
129
|
+
...props
|
|
130
|
+
}: Omit<ZeroOptions<Schema, Mutators>, 'schema' | 'mutators'> & {
|
|
131
|
+
children: ReactNode
|
|
132
|
+
authData?: any
|
|
133
|
+
}) => {
|
|
134
|
+
const mutators = useMemo(() => {
|
|
135
|
+
return createMutators({
|
|
136
|
+
models,
|
|
137
|
+
environment: 'client',
|
|
138
|
+
authData,
|
|
139
|
+
can: permissionsHelpers.can,
|
|
140
|
+
})
|
|
141
|
+
}, [models, authData])
|
|
142
|
+
|
|
143
|
+
if (disable) {
|
|
144
|
+
return children
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<AuthDataContext.Provider value={authData}>
|
|
149
|
+
<ZeroProvider
|
|
150
|
+
schema={schema}
|
|
151
|
+
kvStore={'mem'}
|
|
152
|
+
onError={(error) => {
|
|
153
|
+
console.error(`Zero Error:`, error)
|
|
154
|
+
zeroEmitter.emit({
|
|
155
|
+
type: 'error',
|
|
156
|
+
message: error,
|
|
157
|
+
})
|
|
158
|
+
}}
|
|
159
|
+
mutators={mutators}
|
|
160
|
+
{...props}
|
|
161
|
+
>
|
|
162
|
+
<SetZeroInstance />
|
|
163
|
+
{children}
|
|
164
|
+
</ZeroProvider>
|
|
165
|
+
</AuthDataContext.Provider>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const SetZeroInstance = () => {
|
|
170
|
+
const zero = useZero<Schema, Mutators>()
|
|
171
|
+
|
|
172
|
+
// TODO last hack zero wants us to use useZero but its a big migration
|
|
173
|
+
// and has some downsides (global zero import leads to simpler code)
|
|
174
|
+
// they plan to support .setAuth() at some point, and so long as we refresh
|
|
175
|
+
// when we do change zero, this should be safe - that said we don't refresh
|
|
176
|
+
// the browser for now, but we also don't handle new auth keys in general
|
|
177
|
+
// we'll need to add that soon
|
|
178
|
+
if (zero !== latestZeroInstance) {
|
|
179
|
+
latestZeroInstance = zero
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return null
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
ProvideZero,
|
|
187
|
+
useQuery,
|
|
188
|
+
usePermission,
|
|
189
|
+
zero,
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { Schema as ZeroSchema } from '@rocicorp/zero'
|
|
2
|
+
import type { TransactionProviderInput } from '@rocicorp/zero/pg'
|
|
3
|
+
import { PostgresJSConnection } from '@rocicorp/zero/pg'
|
|
4
|
+
import { ZQLDatabase } from '@rocicorp/zero/server'
|
|
5
|
+
import postgres from 'postgres'
|
|
6
|
+
// import { context, isInZeroMutation } from 'src/context'
|
|
7
|
+
import type { AuthData } from './types'
|
|
8
|
+
// import { type Mutators, createMutators } from '~/data/helpers/createMutators'
|
|
9
|
+
// import { schema } from '~/data/schema'
|
|
10
|
+
// import { createServerActions } from '~/data/server/createServerActions'
|
|
11
|
+
// import type { TX } from '~/data/types'
|
|
12
|
+
import { PushProcessor } from '@rocicorp/zero/pg'
|
|
13
|
+
import { assertString, randomId } from '@vxrn/helpers'
|
|
14
|
+
import { createMutators } from './helpers/createMutators'
|
|
15
|
+
// import type { Endpoint } from 'one'
|
|
16
|
+
// import type { AuthData } from 'start/data'
|
|
17
|
+
// import { getAuthHeader, NotAuthenticatedError } from '~/auth/validateAuthHeader'
|
|
18
|
+
// import { randomId } from '@vxrn/helpers'
|
|
19
|
+
// import { createMutators } from '~/data/helpers/createMutators'
|
|
20
|
+
// import { createServerActions } from '~/data/server/createServerActions'
|
|
21
|
+
// import { GenericTransaction } from './types'
|
|
22
|
+
|
|
23
|
+
// this sets up custom server-side mutators
|
|
24
|
+
// see: https://zero.rocicorp.dev/docs/custom-mutators
|
|
25
|
+
|
|
26
|
+
export function createZeroServer<
|
|
27
|
+
Schema extends ZeroSchema,
|
|
28
|
+
ServerActions extends Record<string, any>,
|
|
29
|
+
>({
|
|
30
|
+
createServerActions,
|
|
31
|
+
schema,
|
|
32
|
+
models,
|
|
33
|
+
disable,
|
|
34
|
+
}: {
|
|
35
|
+
schema: Schema
|
|
36
|
+
models: any
|
|
37
|
+
createServerActions: (authData: AuthData | null) => ServerActions
|
|
38
|
+
disable?: boolean
|
|
39
|
+
}) {
|
|
40
|
+
// TODO
|
|
41
|
+
type TX = any
|
|
42
|
+
type Mutators = any
|
|
43
|
+
|
|
44
|
+
const dbString = assertString(process.env.ZERO_UPSTREAM_DB)
|
|
45
|
+
|
|
46
|
+
const zeroServerDatabase = new ZQLDatabase(
|
|
47
|
+
new PostgresJSConnection(postgres(dbString)),
|
|
48
|
+
schema
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
const processor = new PushProcessor(zeroServerDatabase)
|
|
52
|
+
|
|
53
|
+
const handleMutationRequest = async ({
|
|
54
|
+
authData,
|
|
55
|
+
request,
|
|
56
|
+
skipAsyncTasks,
|
|
57
|
+
}: {
|
|
58
|
+
authData: AuthData
|
|
59
|
+
request: Request
|
|
60
|
+
skipAsyncTasks?: boolean
|
|
61
|
+
}) => {
|
|
62
|
+
// since mutations do DB work in transaction, avoid any async tasks during
|
|
63
|
+
const asyncTasks: Array<() => Promise<void>> = []
|
|
64
|
+
|
|
65
|
+
const response = await processor.process(
|
|
66
|
+
createMutators({
|
|
67
|
+
environment: 'server',
|
|
68
|
+
asyncTasks,
|
|
69
|
+
authData,
|
|
70
|
+
createServerActions,
|
|
71
|
+
}),
|
|
72
|
+
request
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
// now finish
|
|
76
|
+
if (!skipAsyncTasks && asyncTasks.length) {
|
|
77
|
+
const id = randomId()
|
|
78
|
+
console.info(`[push] complete, running async tasks ${asyncTasks.length} id ${id}`)
|
|
79
|
+
Promise.all(asyncTasks.map((task) => task()))
|
|
80
|
+
.then(() => {
|
|
81
|
+
console.info(`[push] async tasks complete ${id}`)
|
|
82
|
+
})
|
|
83
|
+
.catch((err) => {
|
|
84
|
+
console.error(`[push] error: async tasks failed 😞`, err)
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
response,
|
|
90
|
+
asyncTasks,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const serverMutate = async (
|
|
95
|
+
run: (tx: TX, mutators: Mutators) => Promise<void>,
|
|
96
|
+
authData?: Pick<AuthData, 'email' | 'id'> & Partial<AuthData>
|
|
97
|
+
) => {
|
|
98
|
+
const asyncTasks: Array<() => Promise<void>> = []
|
|
99
|
+
|
|
100
|
+
const mutators = createMutators({
|
|
101
|
+
environment: 'server',
|
|
102
|
+
asyncTasks,
|
|
103
|
+
authData: {
|
|
104
|
+
id: '',
|
|
105
|
+
email: 'admin@start.chat',
|
|
106
|
+
role: 'admin',
|
|
107
|
+
...authData,
|
|
108
|
+
},
|
|
109
|
+
createServerActions,
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
await serverTransaction(async (tx) => {
|
|
113
|
+
await run(tx, mutators)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
await Promise.all(asyncTasks.map((t) => t()))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// shorthand but nicer for single queries
|
|
120
|
+
// TODO should unwrap q.query
|
|
121
|
+
const serverQuery = serverTransaction
|
|
122
|
+
|
|
123
|
+
// This is needed temporarily and will be cleaned up in the future.
|
|
124
|
+
const dummyTransactionInput: TransactionProviderInput = {
|
|
125
|
+
clientGroupID: 'unused',
|
|
126
|
+
clientID: 'unused',
|
|
127
|
+
mutationID: 42,
|
|
128
|
+
upstreamSchema: 'unused',
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function serverTransaction<
|
|
132
|
+
CB extends (tx: TX) => Promise<any>,
|
|
133
|
+
Returns extends CB extends (tx: TX) => Promise<infer X> ? X : never,
|
|
134
|
+
>(query: CB): Promise<Returns> {
|
|
135
|
+
try {
|
|
136
|
+
if (isInZeroMutation()) {
|
|
137
|
+
const { tx } = context()
|
|
138
|
+
return await query(tx)
|
|
139
|
+
}
|
|
140
|
+
return (await zeroServerDatabase.transaction(query, dummyTransactionInput)) as any
|
|
141
|
+
} catch (err) {
|
|
142
|
+
console.error(`Error running serverTransaction(): ${err}`)
|
|
143
|
+
throw err
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
handleMutationRequest,
|
|
149
|
+
transaction: serverTransaction,
|
|
150
|
+
mutate: serverMutate,
|
|
151
|
+
query: serverQuery,
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Query, Row } from '@rocicorp/zero'
|
|
2
|
+
import { sleep } from '@vxrn/helpers'
|
|
3
|
+
|
|
4
|
+
export async function batchQuery<Q extends Query<any, any, any>, Item extends Row<Q>>(
|
|
5
|
+
q: Q,
|
|
6
|
+
mapper: (items: Item[]) => Promise<void>,
|
|
7
|
+
{
|
|
8
|
+
chunk,
|
|
9
|
+
pause = 0,
|
|
10
|
+
stopAfter = 100_000,
|
|
11
|
+
}: {
|
|
12
|
+
chunk: number
|
|
13
|
+
pause?: number
|
|
14
|
+
stopAfter?: number
|
|
15
|
+
} = { chunk: 20 }
|
|
16
|
+
) {
|
|
17
|
+
let hasMore = true
|
|
18
|
+
let last: Item | null = null
|
|
19
|
+
let iterations = 0
|
|
20
|
+
|
|
21
|
+
while (hasMore) {
|
|
22
|
+
let query = q.limit(chunk)
|
|
23
|
+
|
|
24
|
+
if (last) {
|
|
25
|
+
query = query.start(last)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const results = await query.run({ type: 'complete' })
|
|
29
|
+
|
|
30
|
+
await mapper(results as Item[])
|
|
31
|
+
|
|
32
|
+
if (results.length < chunk) {
|
|
33
|
+
hasMore = false
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (iterations > stopAfter) {
|
|
37
|
+
console.error(`[batchQuery] ‼️ stopping batch, ran ${stopAfter} chunks`)
|
|
38
|
+
break
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (pause) {
|
|
42
|
+
await sleep(pause)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|