@fabrika/app 0.3.3 → 0.4.1
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/README.md +78 -90
- package/package.json +77 -6
- package/skill/SKILL.md +319 -0
- package/skill/references/app-setup.md +370 -0
- package/skill/references/dev.md +90 -0
- package/skill/references/endpoint.md +640 -0
- package/skill/references/errors.md +108 -0
- package/skill/references/files.md +335 -0
- package/skill/references/jobs.md +169 -0
- package/skill/references/public-intake.md +1495 -0
- package/skill/references/spa.md +746 -0
- package/skill/references/start.md +179 -0
- package/skill/references/table-and-migration.md +264 -0
- package/skill/references/tests.md +308 -0
- package/skill/references/webhooks-links-connectors.md +190 -0
- package/src/agent-skill.ts +77 -0
- package/src/build.ts +53 -0
- package/src/bun.ts +302 -3
- package/src/client/caller.ts +24 -0
- package/src/client/contract.ts +331 -0
- package/src/client/files.ts +135 -0
- package/src/client/index.ts +19 -0
- package/src/client/rpc.ts +47 -0
- package/src/client/share.ts +14 -0
- package/src/{client.ts → client/transport.ts} +58 -61
- package/src/cloudflare.ts +139 -3
- package/src/contract/artifact-schema.ts +317 -0
- package/src/contract/attachment.ts +74 -0
- package/src/contract/build.ts +1932 -0
- package/src/contract/compile.ts +143 -0
- package/src/contract/connect.ts +170 -0
- package/src/contract/context.ts +444 -0
- package/src/contract/declaration.ts +310 -0
- package/src/contract/defaults.ts +161 -0
- package/src/contract/define.ts +567 -0
- package/src/contract/file-schemas.ts +25 -0
- package/src/contract/group.ts +263 -0
- package/src/contract/handled.ts +50 -0
- package/src/contract/index.ts +34 -0
- package/src/contract/kit.ts +607 -0
- package/src/contract/platform.ts +113 -0
- package/src/contract/problems.ts +30 -0
- package/src/contract/schema-from-shape.ts +152 -0
- package/src/contract/schema-shape.ts +407 -0
- package/src/contract/snapshot-source.ts +168 -0
- package/src/contract/vocabulary.ts +50 -0
- package/src/disk-assets.ts +25 -0
- package/src/fetch.ts +10 -0
- package/src/files/archive.ts +292 -0
- package/src/files/clamscan.ts +253 -0
- package/src/files/collections.ts +669 -0
- package/src/files/handle.ts +77 -0
- package/src/files/scan.ts +308 -0
- package/src/files/sniff.ts +70 -0
- package/src/files/treatment.ts +48 -0
- package/src/from-row.ts +53 -0
- package/src/implement/anonymous.ts +102 -0
- package/src/implement/attachment.ts +143 -0
- package/src/implement/boot.ts +824 -0
- package/src/implement/bounded.ts +72 -0
- package/src/implement/browser-config.ts +54 -0
- package/src/implement/capability.ts +439 -0
- package/src/implement/challenge.ts +120 -0
- package/src/implement/connect.ts +386 -0
- package/src/implement/dispatch.ts +1095 -0
- package/src/implement/environment.ts +47 -0
- package/src/implement/failures.ts +116 -0
- package/src/implement/faults.ts +46 -0
- package/src/implement/form-admission.ts +120 -0
- package/src/implement/headers.ts +17 -0
- package/src/implement/implement.ts +81 -0
- package/src/implement/index.ts +60 -0
- package/src/implement/invocation.ts +346 -0
- package/src/implement/membership.ts +485 -0
- package/src/implement/principal.ts +99 -0
- package/src/implement/project.ts +76 -0
- package/src/implement/serve.ts +142 -0
- package/src/implement/types.ts +215 -0
- package/src/implement/webhook.ts +108 -0
- package/src/implement/workload.ts +160 -0
- package/src/implement/xlsx.ts +275 -0
- package/src/index.ts +12 -13
- package/src/jobs/report.ts +134 -0
- package/src/jobs/runner.ts +760 -0
- package/src/jobs/schedule.ts +129 -0
- package/src/jobs/types.ts +44 -0
- package/src/react/caller.tsx +60 -0
- package/src/react/errors.ts +82 -0
- package/src/react/hooks.ts +281 -0
- package/src/react/index.ts +34 -0
- package/src/react/provider.tsx +179 -0
- package/src/react/store.ts +215 -0
- package/src/react/testing.ts +144 -0
- package/src/react/types.ts +40 -0
- package/src/rpc/dispatcher.ts +4 -19
- package/src/service/define.ts +121 -0
- package/src/service/index.ts +21 -0
- package/src/service/serve.ts +99 -0
- package/src/testing/caller.ts +123 -0
- package/src/testing/challenge.ts +10 -0
- package/src/testing/dev.ts +99 -0
- package/src/testing/endpoint-access.ts +809 -0
- package/src/testing/index.ts +36 -0
- package/src/testing/isolation.ts +25 -0
- package/src/testing/kit.ts +585 -0
- package/src/testing/membership.ts +797 -0
- package/src/testing/policy-isolation.ts +827 -0
- package/src/testing/preload.ts +6 -0
- package/src/testing/proxy.ts +87 -0
- package/src/testing/runner.ts +61 -0
- package/src/testing/status.ts +19 -0
- package/src/undumpable.ts +42 -0
- package/src/app.ts +0 -76
- package/src/middleware.ts +0 -38
- package/src/router.ts +0 -149
- package/src/testing.ts +0 -119
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
// What a handler of the staged kit receives. Types only, so the browser-safe contract can name them: every member
|
|
2
|
+
// is typed by the kit (vocabulary, `DbIndex`, imports) and the site's own declaration, never by the contract value,
|
|
3
|
+
// so a group in any module types itself without importing `fabrika.contract.ts`.
|
|
4
|
+
|
|
5
|
+
import type { RpcCaller } from '@fabrika/auth-core'
|
|
6
|
+
import type {
|
|
7
|
+
AnyDbIndex,
|
|
8
|
+
ApplicationTables,
|
|
9
|
+
DataKind,
|
|
10
|
+
FragmentTable,
|
|
11
|
+
NoUnscoped,
|
|
12
|
+
ReferenceTables,
|
|
13
|
+
ScopedApplicationTables,
|
|
14
|
+
ScopeTarget,
|
|
15
|
+
TargetForm,
|
|
16
|
+
Unavailable,
|
|
17
|
+
UnscopedTables,
|
|
18
|
+
WithinTables,
|
|
19
|
+
} from '@fabrika/db/tables'
|
|
20
|
+
import type { z } from 'zod/v4'
|
|
21
|
+
import type { FileCollectionHandle, FileCollectionReadHandle, FileCollections } from '../files/handle'
|
|
22
|
+
import type { AppCapability, Ref, Within } from '../implement/capability'
|
|
23
|
+
import type { ApplicationDirectory, LinkIssuing, MemberDirectory, MemberLookup, MemberManagement } from '../implement/membership'
|
|
24
|
+
import type { AppPrincipal, PrincipalOf, ServicePrincipal } from '../implement/principal'
|
|
25
|
+
import type { ConnectCall, RequestContext } from '../implement/types'
|
|
26
|
+
import type { WebhookDelivery, WebhookPrincipal } from '../implement/webhook'
|
|
27
|
+
import type { JobContext } from '../jobs/types'
|
|
28
|
+
import type { ConnectSnapshot } from './connect'
|
|
29
|
+
import type { MemberAccess, Schema } from './define'
|
|
30
|
+
import type { App } from './kit'
|
|
31
|
+
|
|
32
|
+
export type { Unavailable } from '@fabrika/db/tables'
|
|
33
|
+
|
|
34
|
+
/** One connector the kit imports: the vendored snapshot, the procedures this application calls, its deadline. */
|
|
35
|
+
export interface KitImport {
|
|
36
|
+
readonly snapshot: ConnectSnapshot
|
|
37
|
+
readonly procedures: readonly [string, ...string[]]
|
|
38
|
+
readonly timeoutMs?: number
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type KitImports = { readonly [name: string]: KitImport }
|
|
42
|
+
|
|
43
|
+
/** Everything the kit knows, computed once by `defineApp`: the vocabulary's names, the tables' index, the imports. */
|
|
44
|
+
export interface KitTypes {
|
|
45
|
+
readonly action: string
|
|
46
|
+
readonly dimension: string
|
|
47
|
+
/** The dimensions `creates` may target: top-level ones that declare a `creator` role. */
|
|
48
|
+
readonly creatable: string
|
|
49
|
+
readonly role: string
|
|
50
|
+
/** The dimension definitions as written, for `ctx.members`. */
|
|
51
|
+
readonly dimensions: unknown
|
|
52
|
+
/** The actions of the application's own people, as written; `never` when the vocabulary declares none. */
|
|
53
|
+
readonly members: { readonly manage: string; readonly list: string }
|
|
54
|
+
readonly index: AnyDbIndex
|
|
55
|
+
/** The table names of `src/db/tables.ts`. */
|
|
56
|
+
readonly tables: string
|
|
57
|
+
readonly imports: KitImports
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The index of a kit without tables, `defineApp(vocabulary)`: an application that holds no data of its own (on
|
|
62
|
+
* Cloudflare, which has no data layer yet). Its handlers have no `ctx.db` and no `ctx.sql`.
|
|
63
|
+
*/
|
|
64
|
+
export interface NoTablesIndex {
|
|
65
|
+
readonly '~noTables': true
|
|
66
|
+
readonly tables: Record<never, never>
|
|
67
|
+
readonly owner: Record<never, never>
|
|
68
|
+
readonly children: Record<never, never>
|
|
69
|
+
readonly listed: Record<never, never>
|
|
70
|
+
readonly within: Record<never, never>
|
|
71
|
+
readonly references: never
|
|
72
|
+
readonly applications: never
|
|
73
|
+
readonly records: never
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** `ctx.db` of a site: its handles, or, in a kit without tables, a message that there are none. */
|
|
77
|
+
type DataOf<Ix, Handles> = Ix extends NoTablesIndex ? Unavailable<'this application declares no tables: defineApp(vocabulary, tables) gives ctx.db'>
|
|
78
|
+
: Handles
|
|
79
|
+
|
|
80
|
+
export type ImportOf<K extends KitTypes> = keyof K['imports'] & string
|
|
81
|
+
|
|
82
|
+
/** `sql: 'read' | 'write'`: `ctx.sql` exists only on a site that declares it; `'read'` runs read-only. */
|
|
83
|
+
export type SqlMode = 'read' | 'write'
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The off-road reach a site declares: part of the contract, read by Control, a review signal for the approver. A
|
|
87
|
+
* field the site does not declare is absent.
|
|
88
|
+
*/
|
|
89
|
+
export interface Declared<S extends SqlMode, U, Cn extends readonly string[], Is extends string> {
|
|
90
|
+
readonly sql?: S
|
|
91
|
+
readonly unscoped?: U
|
|
92
|
+
readonly connect?: Cn
|
|
93
|
+
readonly issues?: Is
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** One declared field, `never` when the site leaves it out. */
|
|
97
|
+
type Of<D, F extends keyof Declared<SqlMode, unknown, readonly string[], string>> = D extends { readonly [P in F]?: infer V } ? Exclude<V, undefined>
|
|
98
|
+
: never
|
|
99
|
+
|
|
100
|
+
export type SqlValue = string | number | boolean | bigint | Date | null | readonly (string | number)[]
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* `ctx.sql(Row)\`select …\``: one statement, every interpolation a bound parameter, run in the invocation's
|
|
104
|
+
* transaction; each row is parsed (snake_case → camelCase, `Date` → ISO text, then `Row`).
|
|
105
|
+
*/
|
|
106
|
+
export interface SqlTag<Ix extends AnyDbIndex = AnyDbIndex, M extends SqlMode = SqlMode> {
|
|
107
|
+
<Row extends Schema>(row: Row): (text: TemplateStringsArray, ...values: readonly (SqlValue | SqlScope)[]) => Promise<z.output<Row>[]>
|
|
108
|
+
/**
|
|
109
|
+
* The capability's whole predicate on `table` (`alias` its name in the statement, the table's by default): the
|
|
110
|
+
* object's key and every link of its chain for a `Ref`, what `ctx.db.within` matches for a `Within`, every row for
|
|
111
|
+
* `ctx.app`. Interpolated as `where ${ctx.sql.scope(ctx.target, 'tasks', 't')}`, its values are bound parameters.
|
|
112
|
+
* A table the capability reaches for no use of this site is a type error here; at run time the fragment on the
|
|
113
|
+
* statement's UPDATE or DELETE target needs that operation's action, and anywhere else an action the table reads
|
|
114
|
+
* with.
|
|
115
|
+
*/
|
|
116
|
+
scope<C extends Ref<string, string> | Within<string, string> | AppCapability<string>, T extends FragmentTable<Ix, C, M>>(
|
|
117
|
+
capability: C,
|
|
118
|
+
table: T,
|
|
119
|
+
alias?: string,
|
|
120
|
+
): SqlScope
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
declare const sqlScope: unique symbol
|
|
124
|
+
|
|
125
|
+
/** What `ctx.sql.scope(…)` builds: only the framework makes one, and only its own tag takes it. */
|
|
126
|
+
export interface SqlScope {
|
|
127
|
+
readonly [sqlScope]: true
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
type SqlContext<S, Ix extends AnyDbIndex> = {
|
|
131
|
+
readonly sql: [S] extends [never] ? Unavailable<"ctx.sql needs a declaration: add sql: 'read' (or 'write' on a mutation) to this site">
|
|
132
|
+
: SqlTag<Ix, S & SqlMode>
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** `ctx.files(<collection>)`: removal where the site writes (a mutation, a job's transaction), reading everywhere. */
|
|
136
|
+
type FilesContext<Ix, K extends DataKind> = {
|
|
137
|
+
readonly files: Ix extends NoTablesIndex ? Unavailable<'this application declares no tables, so it has no file collections'>
|
|
138
|
+
: FileCollections<K extends 'mutation' ? FileCollectionHandle : FileCollectionReadHandle<RemovesIn<K>>>
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
type RemovesIn<K extends DataKind> = K extends 'job' ? 'a job removes files inside ctx.transaction((tx) => …), as tx.files'
|
|
142
|
+
: 'a query reads only; remove files in a mutation'
|
|
143
|
+
|
|
144
|
+
type UnscopedData<Ix extends AnyDbIndex, U, K extends DataKind> = {
|
|
145
|
+
readonly unscoped: [U] extends [never] ? NoUnscoped : UnscopedTables<Ix, U, K>
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** An access form the vocabulary refuses: the refusal is reported where the form is written. */
|
|
149
|
+
export interface InvalidAccess {
|
|
150
|
+
readonly '~invalid': 'access'
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
type IsUnion<T, All = T> = T extends unknown ? ([All] extends [T] ? false : true) : never
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* `Acc`, or `InvalidAccess` when it is a union: a written form is one object, so a union is the whole constraint the
|
|
157
|
+
* compiler falls back to after refusing the form. The handler then names the refusal instead of every form.
|
|
158
|
+
*/
|
|
159
|
+
export type CheckedAccess<Acc> = true extends IsUnion<Acc> ? InvalidAccess : Acc
|
|
160
|
+
|
|
161
|
+
/** The capability an access form hands, and what the other forms do not have. */
|
|
162
|
+
export type CapabilityContext<Acc> = Acc extends InvalidAccess ? {
|
|
163
|
+
readonly target: Unavailable<'the access form of this endpoint is refused where it is declared: fix it there'>
|
|
164
|
+
readonly within: Unavailable<'the access form of this endpoint is refused where it is declared: fix it there'>
|
|
165
|
+
readonly created: Unavailable<'the access form of this endpoint is refused where it is declared: fix it there'>
|
|
166
|
+
}
|
|
167
|
+
: Acc extends MemberAccess ? {
|
|
168
|
+
readonly target: Unavailable<'a member endpoint holds no capability: declare access with on or in for ctx.target'>
|
|
169
|
+
readonly within: Unavailable<'a member endpoint holds no capability: declare access with over for ctx.within'>
|
|
170
|
+
}
|
|
171
|
+
: Acc extends { readonly action: infer A extends string; readonly over: infer D extends string } ? {
|
|
172
|
+
readonly within: Within<D, A>
|
|
173
|
+
readonly target: Unavailable<'an over endpoint holds ctx.within, the scopes it may list in: declare access with on or in for ctx.target'>
|
|
174
|
+
}
|
|
175
|
+
: Acc extends { readonly action: infer A extends string; readonly app: true } ? {
|
|
176
|
+
readonly app: AppCapability<A>
|
|
177
|
+
readonly target: Unavailable<'an app endpoint holds ctx.app, its action application-wide: declare access with on or in for ctx.target'>
|
|
178
|
+
readonly within: Unavailable<'an app endpoint holds ctx.app, its action application-wide: declare access with over for ctx.within'>
|
|
179
|
+
}
|
|
180
|
+
: Acc extends { readonly action: infer A extends string; readonly in: infer D extends string } ? { readonly target: Ref<D, A> }
|
|
181
|
+
: Acc extends { readonly action: infer A extends string; readonly on: infer D extends string } ? { readonly target: Ref<D, A> }
|
|
182
|
+
: Acc extends { readonly action: infer A extends string; readonly creates: infer D extends string } ? { readonly created: Ref<D, A> }
|
|
183
|
+
: never
|
|
184
|
+
|
|
185
|
+
type TableName<Ix extends AnyDbIndex> = keyof Ix['tables'] & string
|
|
186
|
+
type TableOf<Ix extends AnyDbIndex, T> = T extends TableName<Ix> ? Ix['tables'][T] : never
|
|
187
|
+
|
|
188
|
+
/** Where a table lies. */
|
|
189
|
+
type Lies<Ix extends AnyDbIndex, T extends string> = TableOf<Ix, T>['mode'] extends 'child' ? `it is a child of ${TableOf<Ix, T>['parent']}`
|
|
190
|
+
: `it lies in ${TableOf<Ix, T>['list']}`
|
|
191
|
+
|
|
192
|
+
/** The access form that reaches a table: its parent's object for a child, else the dimension it lies in. */
|
|
193
|
+
type Reach<Ix extends AnyDbIndex, T extends string, Form extends 'target' | 'over'> = TableOf<Ix, T>['mode'] extends 'child'
|
|
194
|
+
? `declare access on: '${TableOf<Ix, TableOf<Ix, T>['parent']>['dimension']}'`
|
|
195
|
+
: Form extends 'over' ? `declare over: '${TableOf<Ix, T>['list']}'`
|
|
196
|
+
: TableOf<Ix, T>['list'] extends TableOf<Ix, T>['dimension'] ? `declare access on or in: '${TableOf<Ix, T>['list']}'`
|
|
197
|
+
: `declare access in: '${TableOf<Ix, T>['list']}' or on: '${TableOf<Ix, T>['dimension']}'`
|
|
198
|
+
|
|
199
|
+
/** Why the handle `Where` holds no table `T`: one line that names the table, the access form, and the form that reaches it. */
|
|
200
|
+
type Unreached<Ix extends AnyDbIndex, T extends string, Where extends string, Form extends 'target' | 'over'> = T extends Ix['references']
|
|
201
|
+
? Unavailable<`${T} is a reference table: read it through ctx.db.reference.${T}`>
|
|
202
|
+
: TableOf<Ix, T>['mode'] extends 'inbox' ? Unavailable<`${T} is a form inbox, which no ctx.db handle reads`>
|
|
203
|
+
: T extends Ix['applications']
|
|
204
|
+
? Unavailable<`${T} is an application table: read it through ctx.db.app.${T}, or declare access: { action, app: true }`>
|
|
205
|
+
: Unavailable<`${T} is not under ${Where} (${Lies<Ix, T>}): ${Reach<Ix, T, Form>}`>
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* A handle with every other table of the application as a refusal, so `ctx.db.target.tasks` on a form that does not
|
|
209
|
+
* reach `tasks` names the table and the form instead of printing the index.
|
|
210
|
+
*/
|
|
211
|
+
type Reaching<Ix extends AnyDbIndex, H, Where extends string, Form extends 'target' | 'over'> =
|
|
212
|
+
& H
|
|
213
|
+
& { readonly [T in Exclude<TableName<Ix>, keyof H>]: Unreached<Ix, T, Where, Form> }
|
|
214
|
+
|
|
215
|
+
type Target<Ix extends AnyDbIndex, D extends string, A extends string, K extends DataKind, F extends TargetForm> = Reaching<
|
|
216
|
+
Ix,
|
|
217
|
+
ScopeTarget<Ix, D, A, K, F>,
|
|
218
|
+
`${F}: '${D}'`,
|
|
219
|
+
'target'
|
|
220
|
+
>
|
|
221
|
+
|
|
222
|
+
type WithinData<Ix extends AnyDbIndex, D extends string, A extends string> = Reaching<Ix, WithinTables<Ix, D, A>, `over: '${D}'`, 'over'>
|
|
223
|
+
|
|
224
|
+
type AppData<Ix extends AnyDbIndex, A extends string, K extends DataKind> = Reaching<Ix, ApplicationTables<Ix, A, K>, 'app: true', 'over'>
|
|
225
|
+
|
|
226
|
+
/** `ctx.db.app` of an `on`, `in` or `over` endpoint: the application tables it reads; every other table names its handle. */
|
|
227
|
+
type ScopedAppData<Ix extends AnyDbIndex, A extends string> =
|
|
228
|
+
& ScopedApplicationTables<Ix, A>
|
|
229
|
+
& {
|
|
230
|
+
readonly [T in Exclude<TableName<Ix>, Ix['applications']>]: Unavailable<
|
|
231
|
+
`${T} is not an application table: a scoped endpoint's ctx.db.app reads application tables only`
|
|
232
|
+
>
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** `ctx.db` by access form (the API matrix of `docs/reference/application-runtime.md`). */
|
|
236
|
+
type AccessData<Ix extends AnyDbIndex, K extends DataKind, Acc> = Acc extends InvalidAccess ? {
|
|
237
|
+
readonly target: Unavailable<'the access form of this endpoint is refused where it is declared: fix it there'>
|
|
238
|
+
readonly within: Unavailable<'the access form of this endpoint is refused where it is declared: fix it there'>
|
|
239
|
+
readonly created: Unavailable<'the access form of this endpoint is refused where it is declared: fix it there'>
|
|
240
|
+
}
|
|
241
|
+
: Acc extends MemberAccess ? {
|
|
242
|
+
readonly target: Unavailable<'a member endpoint reaches no application data: declare access with on, in or over'>
|
|
243
|
+
}
|
|
244
|
+
: Acc extends { readonly action: infer A extends string; readonly over: infer D extends string } ? {
|
|
245
|
+
readonly within: WithinData<Ix, D, A>
|
|
246
|
+
readonly app: ScopedAppData<Ix, A>
|
|
247
|
+
readonly target: Unavailable<'an over endpoint lists through ctx.db.within: declare access with on or in for ctx.db.target'>
|
|
248
|
+
}
|
|
249
|
+
: Acc extends { readonly action: infer A extends string; readonly app: true } ? {
|
|
250
|
+
readonly app: AppData<Ix, A, K>
|
|
251
|
+
readonly target: Unavailable<'an app endpoint reaches its data through ctx.db.app: declare access with on or in for ctx.db.target'>
|
|
252
|
+
readonly within: Unavailable<'an app endpoint reaches its data through ctx.db.app: declare access with over for ctx.db.within'>
|
|
253
|
+
}
|
|
254
|
+
: Acc extends { readonly action: infer A extends string; readonly in: infer D extends string }
|
|
255
|
+
? { readonly target: Target<Ix, D, A, K, 'in'>; readonly app: ScopedAppData<Ix, A> }
|
|
256
|
+
: Acc extends { readonly action: infer A extends string; readonly on: infer D extends string }
|
|
257
|
+
? { readonly target: Target<Ix, D, A, K, 'on'>; readonly app: ScopedAppData<Ix, A> }
|
|
258
|
+
: Acc extends { readonly action: infer A extends string; readonly creates: infer D extends string } ? { readonly created: Target<Ix, D, A, K, 'in'> }
|
|
259
|
+
: never
|
|
260
|
+
|
|
261
|
+
/** `ctx.db` of an endpoint: its access form's handle, the reference tables, and the tables it declares unscoped. */
|
|
262
|
+
export type EndpointData<Ix extends AnyDbIndex, K extends DataKind, Acc, U> =
|
|
263
|
+
& AccessData<Ix, K, Acc>
|
|
264
|
+
& { readonly reference: ReferenceTables<Ix> }
|
|
265
|
+
& UnscopedData<Ix, U, K>
|
|
266
|
+
|
|
267
|
+
/** The dimension and action of an access form that hands a `Ref` of an existing object or scope. */
|
|
268
|
+
type TargetOf<Acc> = Acc extends { readonly action: infer A extends string; readonly in: infer D extends string } ? readonly [D, A]
|
|
269
|
+
: Acc extends { readonly action: infer A extends string; readonly on: infer D extends string } ? readonly [D, A]
|
|
270
|
+
: never
|
|
271
|
+
|
|
272
|
+
type MembersOf<K extends KitTypes, D extends string> = D extends keyof K['dimensions']
|
|
273
|
+
? K['dimensions'][D] extends { readonly members: { readonly manage: infer M extends string; readonly list: infer L extends string } }
|
|
274
|
+
? { readonly manage: M; readonly list: L }
|
|
275
|
+
: never
|
|
276
|
+
: never
|
|
277
|
+
|
|
278
|
+
/** The nearest dimension, `D` or one containing it, that declares members; `never` when none does. */
|
|
279
|
+
type DirectoryOf<K extends KitTypes, D extends string> = [MembersOf<K, D>] extends [never]
|
|
280
|
+
? D extends keyof K['dimensions'] ? K['dimensions'][D] extends { readonly in: infer P extends string } ? DirectoryOf<K, P> : never : never
|
|
281
|
+
: D
|
|
282
|
+
|
|
283
|
+
/** A member's label in the directory of `N`: IAM answers one to every caller where `N` declares `directory: 'labels'`. */
|
|
284
|
+
type LabelOf<K extends KitTypes, N extends string> = N extends keyof K['dimensions']
|
|
285
|
+
? K['dimensions'][N] extends { readonly directory: 'labels' } ? string : string | null
|
|
286
|
+
: string | null
|
|
287
|
+
|
|
288
|
+
/** `ctx.members` below a directory `N`: the list where the action is its `members.list`, the lookup of one member everywhere. */
|
|
289
|
+
type DirectoryContext<K extends KitTypes, N extends string, A extends string> = A extends MembersOf<K, N>['list']
|
|
290
|
+
? { readonly members: MemberDirectory<LabelOf<K, N>> }
|
|
291
|
+
: {
|
|
292
|
+
readonly members:
|
|
293
|
+
& MemberLookup<LabelOf<K, N>>
|
|
294
|
+
& { readonly list: Unavailable<`listing the members of ${N} needs the action '${MembersOf<K, N>['list']}'; get(principalId) checks one`> }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* `ctx.members` of an endpoint on or in a dimension: management where the action is that dimension's `members.manage`;
|
|
299
|
+
* otherwise the directory of the nearest dimension, its own or one containing it, that declares members. IAM checks the
|
|
300
|
+
* endpoint's own action for a lookup and `members.list` for a list, so the type promises what IAM answers.
|
|
301
|
+
*/
|
|
302
|
+
type MembersContext<K extends KitTypes, Acc> = Acc extends { readonly action: infer A extends string; readonly app: true }
|
|
303
|
+
? ApplicationMembersContext<K, A>
|
|
304
|
+
: TargetOf<Acc> extends readonly [infer D extends string, infer A extends string]
|
|
305
|
+
? A extends MembersOf<K, D>['manage'] ? { readonly members: MemberManagement<D, A, K['role']> }
|
|
306
|
+
: [DirectoryOf<K, D>] extends [never] ? { readonly members: Unavailable<`neither ${D} nor a dimension containing it declares members`> }
|
|
307
|
+
: DirectoryContext<K, DirectoryOf<K, D>, A>
|
|
308
|
+
: { readonly members: Unavailable<'ctx.members needs access on or in a dimension, with the action that lists or manages its members'> }
|
|
309
|
+
|
|
310
|
+
/** `ctx.members` of an `app: true` endpoint: the application's own people, where its action is their `members.list`. */
|
|
311
|
+
type ApplicationMembersContext<K extends KitTypes, A extends string> = [K['members']] extends [never]
|
|
312
|
+
? { readonly members: Unavailable<'the application declares no people: members: { manage, list } in the vocabulary'> }
|
|
313
|
+
: A extends K['members']['list'] ? { readonly members: ApplicationDirectory }
|
|
314
|
+
: { readonly members: Unavailable<`listing the application's people needs the action '${K['members']['list']}'`> }
|
|
315
|
+
|
|
316
|
+
/** `ctx.links`, only where the endpoint declares the link mount it issues (`issues: '<mount>'`). */
|
|
317
|
+
type LinksContext<Acc, Is extends string> = [Is] extends [never]
|
|
318
|
+
? { readonly links: Unavailable<"declare issues: '<link mount>' on this endpoint to issue its share links"> }
|
|
319
|
+
: TargetOf<Acc> extends readonly [infer D extends string, infer A extends string] ? { readonly links: LinkIssuing<D, A, Is> }
|
|
320
|
+
: { readonly links: Unavailable<'a share link is bound to one object: declare access with on or in'> }
|
|
321
|
+
|
|
322
|
+
type ConnectorOf<I> = I extends { readonly snapshot: infer S; readonly procedures: readonly (infer P extends string)[] } ? {
|
|
323
|
+
readonly [Path in P]: S extends
|
|
324
|
+
{ readonly procedures: { readonly [Q in Path]: { readonly input: infer In; readonly output: infer O extends Schema } } } ? ConnectCall<In, O>
|
|
325
|
+
: Unavailable<`the snapshot has no procedure ${Path}`>
|
|
326
|
+
}
|
|
327
|
+
: never
|
|
328
|
+
|
|
329
|
+
/** `ctx.connect`, with the connectors the endpoint declares (`connect: ['<connector>']`). */
|
|
330
|
+
type ConnectContext<K extends KitTypes, Cn extends readonly string[]> = [Cn] extends [never]
|
|
331
|
+
? { readonly connect: Unavailable<"declare connect: ['<connector>'] on this endpoint to call another application"> }
|
|
332
|
+
: { readonly connect: { readonly [N in Cn[number]]: ConnectorOf<K['imports'][N]> } }
|
|
333
|
+
|
|
334
|
+
/** What the handler of an RPC endpoint receives, for mount callers `Caller`, kind `Kind` and its declaration. */
|
|
335
|
+
export type EndpointContext<
|
|
336
|
+
K extends KitTypes,
|
|
337
|
+
Caller extends RpcCaller,
|
|
338
|
+
Kind extends DataKind,
|
|
339
|
+
Acc,
|
|
340
|
+
D extends Declared<SqlMode, unknown, readonly string[], string>,
|
|
341
|
+
> =
|
|
342
|
+
& RequestContext<PrincipalOf<Caller>>
|
|
343
|
+
& CapabilityContext<Acc>
|
|
344
|
+
& MembersContext<K, Acc>
|
|
345
|
+
& LinksContext<Acc, Of<D, 'issues'>>
|
|
346
|
+
& ConnectContext<K, Of<D, 'connect'>>
|
|
347
|
+
& SqlContext<Of<D, 'sql'>, K['index']>
|
|
348
|
+
& FilesContext<K['index'], Kind>
|
|
349
|
+
& { readonly db: DataOf<K['index'], EndpointData<K['index'], Kind, Acc, Of<D, 'unscoped'>>> }
|
|
350
|
+
|
|
351
|
+
/** A link procedure acts on the one object its key is bound to. */
|
|
352
|
+
export type LinkContext<
|
|
353
|
+
K extends KitTypes,
|
|
354
|
+
Kind extends DataKind,
|
|
355
|
+
LD extends string,
|
|
356
|
+
A extends string,
|
|
357
|
+
D extends Declared<SqlMode, unknown, never, never>,
|
|
358
|
+
> =
|
|
359
|
+
& RequestContext<PrincipalOf<'link'>>
|
|
360
|
+
& { readonly target: Ref<LD, A> }
|
|
361
|
+
& SqlContext<Of<D, 'sql'>, K['index']>
|
|
362
|
+
& FilesContext<K['index'], Kind>
|
|
363
|
+
& {
|
|
364
|
+
readonly db: DataOf<K['index'], EndpointData<K['index'], Kind, { readonly action: A; readonly on: LD; readonly from: string }, Of<D, 'unscoped'>>>
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** An export procedure runs read-only, for a consumer application: its capability and data, nothing that writes. */
|
|
368
|
+
export type ExportContext<K extends KitTypes, Acc, D extends Declared<'read', unknown, never, never>> =
|
|
369
|
+
& RequestContext<ServicePrincipal | AppPrincipal>
|
|
370
|
+
& CapabilityContext<Acc>
|
|
371
|
+
& SqlContext<Of<D, 'sql'>, K['index']>
|
|
372
|
+
& { readonly db: DataOf<K['index'], EndpointData<K['index'], 'query', Acc, Of<D, 'unscoped'>>> }
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* A capability a job or webhook minted or received, of one of its declared actions `Acts`, bound to the same handles
|
|
376
|
+
* an endpoint's access form gets. A capability typed with several actions reads no column of another read action.
|
|
377
|
+
*/
|
|
378
|
+
export interface CapabilityData<Ix extends AnyDbIndex, Kind extends DataKind, Acts extends string> {
|
|
379
|
+
on<const D extends string, const A extends Acts>(ref: Ref<D, A>): Target<Ix, D, A, Kind, 'on'>
|
|
380
|
+
in<const D extends string, const A extends Acts>(ref: Ref<D, A>): Target<Ix, D, A, Kind, 'in'>
|
|
381
|
+
over<const D extends string, const A extends Acts>(within: Within<D, A>): WithinData<Ix, D, A>
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
type SiteData<Ix extends AnyDbIndex, Kind extends DataKind, Acts extends string, U> =
|
|
385
|
+
& CapabilityData<Ix, Kind, Acts>
|
|
386
|
+
& { readonly reference: ReferenceTables<Ix> }
|
|
387
|
+
& UnscopedData<Ix, U, Kind>
|
|
388
|
+
|
|
389
|
+
/** The fenced transaction of a job attempt: writes and `tx.sql` commit only while the attempt holds its lease. */
|
|
390
|
+
export type JobTransaction<K extends KitTypes, A extends string, D extends Declared<SqlMode, unknown, never, string>> =
|
|
391
|
+
& { readonly db: SiteData<K['index'], 'mutation', A, Of<D, 'unscoped'>> }
|
|
392
|
+
& SqlContext<Of<D, 'sql'>, K['index']>
|
|
393
|
+
& FilesContext<K['index'], 'mutation'>
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* `ctx.links` of a cron job that declares the link mount it issues (`issues: '<mount>'`): a link for an object the job
|
|
397
|
+
* located with `ctx.ref(<the mount's issuedBy>, <its dimension>, id)`, and the revocation of a link it issued.
|
|
398
|
+
*/
|
|
399
|
+
type JobLinksContext<Dim extends string, A extends string, Is extends string> = [Is] extends [never]
|
|
400
|
+
? { readonly links: Unavailable<"declare issues: '<link mount>' on this job to issue its share links"> }
|
|
401
|
+
: { readonly links: LinkIssuing<Dim, A, Is> }
|
|
402
|
+
|
|
403
|
+
/** What a cron job's handler receives. Its reads run outside the transaction; every write runs in `ctx.transaction`. */
|
|
404
|
+
export type JobKitContext<K extends KitTypes, A extends string, D extends Declared<SqlMode, unknown, never, string>> =
|
|
405
|
+
& JobContext<A, K['dimension']>
|
|
406
|
+
& { readonly db: SiteData<K['index'], 'job', A, Of<D, 'unscoped'>> }
|
|
407
|
+
& { readonly sql: Unavailable<'a job runs ctx.sql inside ctx.transaction((tx) => …), as tx.sql'> }
|
|
408
|
+
& FilesContext<K['index'], 'job'>
|
|
409
|
+
& JobLinksContext<K['dimension'], A, Of<D, 'issues'>>
|
|
410
|
+
& { transaction<R>(work: (tx: JobTransaction<K, A, D>) => Promise<R>): Promise<R> }
|
|
411
|
+
|
|
412
|
+
/** What a webhook delivery's handler receives: one transaction per delivery, idempotent by `ctx.delivery.key`. */
|
|
413
|
+
export type WebhookKitContext<K extends KitTypes, B extends string, Acts extends string, D extends Declared<SqlMode, unknown, never, never>> =
|
|
414
|
+
& RequestContext<WebhookPrincipal>
|
|
415
|
+
& { readonly refs: { readonly [A in Acts]: Ref<B, A> }; readonly delivery: WebhookDelivery }
|
|
416
|
+
& SqlContext<Of<D, 'sql'>, K['index']>
|
|
417
|
+
& { readonly db: SiteData<K['index'], 'mutation', Acts, Of<D, 'unscoped'>> }
|
|
418
|
+
|
|
419
|
+
type IndexOf<A> = A extends App<infer K> ? K['index'] : never
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* The type of a `ctx.db` handle, for a function of `src/<slice>/queries.ts` that takes it as a parameter:
|
|
423
|
+
* `DbHandle<typeof app, 'on', 'deal', 'deals.write', 'mutation'>` is `ctx.db.target` of a mutation `on` a deal with
|
|
424
|
+
* `deals.write`, and `DbHandle<typeof app, 'over', 'org', 'crm.read'>` is `ctx.db.within` of a query `over` orgs.
|
|
425
|
+
*/
|
|
426
|
+
export type DbHandle<A, F extends TargetForm | 'over', D extends string, Act extends string, K extends DataKind = 'query'> = F extends TargetForm
|
|
427
|
+
? Target<IndexOf<A>, D, Act, K, F>
|
|
428
|
+
: WithinData<IndexOf<A>, D, Act>
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* The type of `ctx.db.app` of an `app` endpoint, for a function of `src/<slice>/queries.ts` that takes it:
|
|
432
|
+
* `AppHandle<typeof app, 'candidates.export', 'mutation'>`.
|
|
433
|
+
*/
|
|
434
|
+
export type AppHandle<A, Act extends string, K extends DataKind = 'query'> = AppData<IndexOf<A>, Act, K>
|
|
435
|
+
|
|
436
|
+
/** The type of `ctx.db.reference`, for a query function that takes it. */
|
|
437
|
+
export type ReferenceHandle<A> = ReferenceTables<IndexOf<A>>
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* The type of `ctx.sql`, for a function of `src/<slice>/queries.ts` that takes it: `SqlHandle<typeof app, 'read'>`.
|
|
441
|
+
* A `'write'` site's `ctx.sql` is assignable to it, and `sql.scope(capability, table)` checks the table against the
|
|
442
|
+
* app's own tables.
|
|
443
|
+
*/
|
|
444
|
+
export type SqlHandle<A, M extends SqlMode> = SqlTag<IndexOf<A>, M>
|