@fabrika/app 0.9.0-dev.1 → 0.9.0-dev.3
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/package.json +27 -26
- package/skill/SKILL.md +1 -1
- package/skill/references/app-setup.md +8 -8
- package/src/dev/changes.ts +5 -43
- package/src/dev/contract-digest.ts +17 -0
- package/src/dev/index.ts +1 -1
- package/src/dev/launcher.ts +116 -31
- package/src/dev/wire.ts +11 -1
- package/types/src/agent-skill.d.ts +29 -0
- package/types/src/build.d.ts +11 -0
- package/types/src/bun.d.ts +66 -0
- package/types/src/client/caller.d.ts +19 -0
- package/types/src/client/contract.d.ts +131 -0
- package/types/src/client/error-reporting.d.ts +66 -0
- package/types/src/client/index.d.ts +4 -0
- package/types/src/client/rpc.d.ts +14 -0
- package/types/src/client/share.d.ts +2 -0
- package/types/src/client/transport.d.ts +42 -0
- package/types/src/cloudflare.d.ts +49 -0
- package/types/src/contract/artifact-schema.d.ts +8 -0
- package/types/src/contract/attachment.d.ts +41 -0
- package/types/src/contract/build.d.ts +12 -0
- package/types/src/contract/compile.d.ts +11 -0
- package/types/src/contract/connect.d.ts +51 -0
- package/types/src/contract/context.d.ts +501 -0
- package/types/src/contract/declaration.d.ts +303 -0
- package/types/src/contract/defaults.d.ts +208 -0
- package/types/src/contract/define.d.ts +359 -0
- package/types/src/contract/file.d.ts +44 -0
- package/types/src/contract/group.d.ts +274 -0
- package/types/src/contract/handled.d.ts +18 -0
- package/types/src/contract/index.d.ts +19 -0
- package/types/src/contract/kit.d.ts +210 -0
- package/types/src/contract/platform.d.ts +9 -0
- package/types/src/contract/problems.d.ts +14 -0
- package/types/src/contract/redirect.d.ts +25 -0
- package/types/src/contract/schema-from-shape.d.ts +10 -0
- package/types/src/contract/schema-shape.d.ts +5 -0
- package/types/src/contract/snapshot-source.d.ts +15 -0
- package/types/src/contract/token.d.ts +58 -0
- package/types/src/contract/vocabulary.d.ts +30 -0
- package/types/src/contract/webhook-binding.d.ts +24 -0
- package/types/src/contract/webhook-scheme.d.ts +23 -0
- package/types/src/dev/changes.d.ts +7 -0
- package/types/src/dev/contract-digest.d.ts +1 -0
- package/types/src/dev/index.d.ts +2 -0
- package/types/src/dev/launcher.d.ts +32 -0
- package/types/src/dev/output.d.ts +22 -0
- package/types/src/dev/wire.d.ts +126 -0
- package/types/src/disk-assets.d.ts +3 -0
- package/types/src/errors.d.ts +46 -0
- package/types/src/fetch.d.ts +7 -0
- package/types/src/files/archive.d.ts +26 -0
- package/types/src/files/clamscan.d.ts +32 -0
- package/types/src/files/handle.d.ts +106 -0
- package/types/src/files/kinds.d.ts +105 -0
- package/types/src/files/scan.d.ts +80 -0
- package/types/src/files/sniff.d.ts +7 -0
- package/types/src/files/streams.d.ts +17 -0
- package/types/src/files/treatment.d.ts +18 -0
- package/types/src/from-row.d.ts +20 -0
- package/types/src/honeypot.d.ts +2 -0
- package/types/src/implement/anonymous.d.ts +26 -0
- package/types/src/implement/attachment.d.ts +14 -0
- package/types/src/implement/boot.d.ts +109 -0
- package/types/src/implement/bounded.d.ts +22 -0
- package/types/src/implement/browser-config.d.ts +19 -0
- package/types/src/implement/capability.d.ts +140 -0
- package/types/src/implement/challenge.d.ts +75 -0
- package/types/src/implement/client-address.d.ts +11 -0
- package/types/src/implement/connect.d.ts +67 -0
- package/types/src/implement/crashes.d.ts +31 -0
- package/types/src/implement/dispatch.d.ts +218 -0
- package/types/src/implement/egress.d.ts +14 -0
- package/types/src/implement/environment.d.ts +14 -0
- package/types/src/implement/failures.d.ts +34 -0
- package/types/src/implement/faults.d.ts +44 -0
- package/types/src/implement/headers.d.ts +7 -0
- package/types/src/implement/implement.d.ts +58 -0
- package/types/src/implement/index.d.ts +24 -0
- package/types/src/implement/invocation.d.ts +120 -0
- package/types/src/implement/membership.d.ts +201 -0
- package/types/src/implement/multipart.d.ts +36 -0
- package/types/src/implement/params.d.ts +7 -0
- package/types/src/implement/principal.d.ts +52 -0
- package/types/src/implement/project.d.ts +11 -0
- package/types/src/implement/public.d.ts +61 -0
- package/types/src/implement/serve.d.ts +25 -0
- package/types/src/implement/tokens.d.ts +49 -0
- package/types/src/implement/types.d.ts +243 -0
- package/types/src/implement/urls.d.ts +15 -0
- package/types/src/implement/verifiers.d.ts +19 -0
- package/types/src/implement/webhook.d.ts +55 -0
- package/types/src/implement/workload.d.ts +56 -0
- package/types/src/implement/xlsx.d.ts +6 -0
- package/types/src/index.d.ts +12 -0
- package/types/src/jobs/exception.d.ts +40 -0
- package/types/src/jobs/mail.d.ts +67 -0
- package/types/src/jobs/report.d.ts +56 -0
- package/types/src/jobs/runner.d.ts +145 -0
- package/types/src/jobs/schedule.d.ts +15 -0
- package/types/src/jobs/scrub.d.ts +12 -0
- package/types/src/jobs/types.d.ts +92 -0
- package/types/src/react/caller.d.ts +30 -0
- package/types/src/react/errors.d.ts +40 -0
- package/types/src/react/hooks.d.ts +110 -0
- package/types/src/react/index.d.ts +5 -0
- package/types/src/react/provider.d.ts +58 -0
- package/types/src/react/store.d.ts +68 -0
- package/types/src/react/testing.d.ts +28 -0
- package/types/src/react/types.d.ts +33 -0
- package/types/src/rpc/builder.d.ts +52 -0
- package/types/src/rpc/contract.d.ts +23 -0
- package/types/src/rpc/dispatcher.d.ts +11 -0
- package/types/src/rpc/types.d.ts +62 -0
- package/types/src/service/define.d.ts +42 -0
- package/types/src/service/index.d.ts +3 -0
- package/types/src/service/serve.d.ts +28 -0
- package/types/src/standard-schema.d.ts +55 -0
- package/types/src/testing/caller.d.ts +95 -0
- package/types/src/testing/challenge.d.ts +11 -0
- package/types/src/testing/dev.d.ts +97 -0
- package/types/src/testing/endpoint-access.d.ts +99 -0
- package/types/src/testing/index.d.ts +12 -0
- package/types/src/testing/isolation.d.ts +3 -0
- package/types/src/testing/kit.d.ts +182 -0
- package/types/src/testing/membership.d.ts +132 -0
- package/types/src/testing/policy-isolation.d.ts +113 -0
- package/types/src/testing/preload.d.ts +1 -0
- package/types/src/testing/proxy.d.ts +40 -0
- package/types/src/testing/runner.d.ts +8 -0
- package/types/src/testing/sample-file.d.ts +5 -0
- package/types/src/testing/status.d.ts +4 -0
- package/types/src/undumpable.d.ts +5 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { IamMembership } from '@fabrika/auth';
|
|
2
|
+
import { type ContractArtifact } from '@fabrika/auth-core';
|
|
3
|
+
import type { CapabilityAuthority } from '@fabrika/db/capability';
|
|
4
|
+
import { type DataAccess, type FrameworkTables } from '@fabrika/db/framework';
|
|
5
|
+
import { type StoredFileKinds } from '../files/kinds';
|
|
6
|
+
import { type CapabilityMint } from '../implement/capability';
|
|
7
|
+
import type { ContractDatabase } from '../implement/implement';
|
|
8
|
+
import type { SignedTokens } from '../implement/tokens';
|
|
9
|
+
import { type AppUrl } from '../implement/urls';
|
|
10
|
+
import { type WorkloadCredential } from '../implement/workload';
|
|
11
|
+
import { type MailTransports } from './mail';
|
|
12
|
+
import type { OperationsReporter } from './report';
|
|
13
|
+
/**
|
|
14
|
+
* The lease outlives an attempt's deadline by this much: the runner renews or finishes a timed-out attempt's lease,
|
|
15
|
+
* and no other run takes it over meanwhile. The attempt's writes stop at its deadline regardless.
|
|
16
|
+
*/
|
|
17
|
+
export declare const LEASE_GRACE_MS = 60000;
|
|
18
|
+
/** How often the serving process checks every job for a crashed run and for a schedule that stopped. */
|
|
19
|
+
export declare const JOB_CHECK_INTERVAL_MS = 60000;
|
|
20
|
+
/**
|
|
21
|
+
* Why a run failed. Fixed codes only: an application error's name and message may carry a secret, so neither is
|
|
22
|
+
* ever logged or returned. Only the report to Operations carries them, scrubbed (ADR-0059).
|
|
23
|
+
*/
|
|
24
|
+
export type JobFailureCode = 'timeout' | 'lease_lost' | 'handler_failed'
|
|
25
|
+
/** The data layer refused a statement or an operation tree (`QueryRefusedError`): its reason names why. */
|
|
26
|
+
| 'query_refused'
|
|
27
|
+
/** A capability or a handle was refused: forged, ended, of other work, or of another kind than the call takes. */
|
|
28
|
+
| 'capability_refused' | 'driver_failed' | 'crashed'
|
|
29
|
+
/** IAM refused the job's key: the attempt mints no capability. */
|
|
30
|
+
| 'unauthorized'
|
|
31
|
+
/** IAM could not be asked for the job's token. */
|
|
32
|
+
| 'iam_unavailable'
|
|
33
|
+
/** The process stopped a kicked run while shutting down; the schedule runs the job again. */
|
|
34
|
+
| 'stopped'
|
|
35
|
+
/** The transport refused a message the handler did not catch; `reason` is the `EmailDeliveryError` code. */
|
|
36
|
+
| 'mail_failed';
|
|
37
|
+
export type JobRunResult = {
|
|
38
|
+
readonly status: 'succeeded';
|
|
39
|
+
readonly attempts: number;
|
|
40
|
+
}
|
|
41
|
+
/** `reason` is a framework refusal's catalog key (`query_refused`, `capability_refused`), present only for one. */
|
|
42
|
+
| {
|
|
43
|
+
readonly status: 'failed';
|
|
44
|
+
readonly attempts: number;
|
|
45
|
+
readonly code: JobFailureCode;
|
|
46
|
+
readonly reason?: string;
|
|
47
|
+
}
|
|
48
|
+
/** Another run holds the lease: an overlapping run does not start. */
|
|
49
|
+
| {
|
|
50
|
+
readonly status: 'skipped';
|
|
51
|
+
};
|
|
52
|
+
/** The runner's waits; the lease's times are always the database's. */
|
|
53
|
+
export interface JobClock {
|
|
54
|
+
/** Resolves after `ms`, or as soon as `signal` aborts. */
|
|
55
|
+
wait(ms: number, signal: AbortSignal): Promise<void>;
|
|
56
|
+
/** In [0, 1): the jitter of a retry's backoff. */
|
|
57
|
+
random(): number;
|
|
58
|
+
}
|
|
59
|
+
export interface JobRunOptions {
|
|
60
|
+
readonly clock?: JobClock;
|
|
61
|
+
/** Where failures and missed runs go; by default the one `boot` built from `FABRIKA_OPERATIONS_DSN`. */
|
|
62
|
+
readonly reporter?: OperationsReporter;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A job the framework runs itself under a reserved name (`FILE_SCAN_JOB`): no handler, no key, no capability, one
|
|
66
|
+
* attempt. It holds its lease like an application job, and every write of its run is fenced by it.
|
|
67
|
+
*/
|
|
68
|
+
export interface FrameworkJob {
|
|
69
|
+
readonly name: string;
|
|
70
|
+
readonly schedule: string;
|
|
71
|
+
readonly timeoutMs: number;
|
|
72
|
+
/** One run; `signal` aborts when the attempt's deadline passes. A throw fails the run. */
|
|
73
|
+
run(reporter: OperationsReporter, signal: AbortSignal): Promise<void>;
|
|
74
|
+
}
|
|
75
|
+
/** The contract's jobs, bound to their handlers by `boot`, and the framework's own. */
|
|
76
|
+
export interface ContractJobs {
|
|
77
|
+
/** The contract's `c.cron` jobs; `run` also takes the name of a framework job the contract needs. */
|
|
78
|
+
readonly names: readonly string[];
|
|
79
|
+
run(name: string, options?: JobRunOptions): Promise<JobRunResult>;
|
|
80
|
+
/**
|
|
81
|
+
* Registers every job (its first-seen time), reports a run that crashed and a job that has not succeeded within
|
|
82
|
+
* twice its period. The serving process calls it every `JOB_CHECK_INTERVAL_MS`; it never throws.
|
|
83
|
+
*/
|
|
84
|
+
check(options?: Pick<JobRunOptions, 'reporter'>): Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
/** A run that cannot start: an unknown job. Its message is framework-authored. */
|
|
87
|
+
export declare class JobError extends Error {
|
|
88
|
+
readonly name = "JobError";
|
|
89
|
+
}
|
|
90
|
+
export declare const systemClock: JobClock;
|
|
91
|
+
/** Where `fabrika app dev` shows what a handler threw, on the developer's own machine; nothing else sets one. */
|
|
92
|
+
export type HandlerErrorSink = (where: string, error: unknown) => void;
|
|
93
|
+
export interface JobBindingInput {
|
|
94
|
+
readonly artifact: ContractArtifact;
|
|
95
|
+
/** The handler tree `boot` binds; a job's handler is `handlers.jobs.<name>`. */
|
|
96
|
+
readonly handlers: unknown;
|
|
97
|
+
readonly database: ContractDatabase | null;
|
|
98
|
+
/** The framework side of `@fabrika/db`, bound by `boot`; `null` when the application has no such layer. */
|
|
99
|
+
readonly framework: FrameworkTables | null;
|
|
100
|
+
readonly ownedDimensions: ReadonlySet<string>;
|
|
101
|
+
/** Where failures and missed runs go: Operations, or the server log without a DSN. */
|
|
102
|
+
readonly reporter: OperationsReporter;
|
|
103
|
+
/** `job:<name>` → the job's key; `boot` refuses a job without one. */
|
|
104
|
+
readonly credentials: ReadonlyMap<string, WorkloadCredential>;
|
|
105
|
+
/** The runtime's capability mint, which `boot` alone holds. */
|
|
106
|
+
readonly mint: CapabilityMint;
|
|
107
|
+
/** The mini-ORM of the bound `scoped(tables)` layer, behind a staged-kit job's `ctx.db`. */
|
|
108
|
+
readonly data: DataAccess | null;
|
|
109
|
+
/** The contract's file kinds, behind a staged-kit job's `ctx.files.put` and `.open`; `null` without any. */
|
|
110
|
+
readonly fileKinds?: StoredFileKinds | null;
|
|
111
|
+
/** The framework's own jobs this contract needs: the scan of its `scan: true` file kinds, and the file sweep. */
|
|
112
|
+
readonly frameworkJobs?: readonly FrameworkJob[];
|
|
113
|
+
/** IAM's membership operations, which a job that `issues` links asks; `null` when the adapter supplies none. */
|
|
114
|
+
readonly membership: IamMembership | null;
|
|
115
|
+
/** The runtime's authority, which a `Ref` passed to IAM must resolve with. */
|
|
116
|
+
readonly capabilities: CapabilityAuthority;
|
|
117
|
+
/** Each dimension's containers, nearest first: a link's chain. */
|
|
118
|
+
readonly containers: ReadonlyMap<string, readonly string[]>;
|
|
119
|
+
/** What a job's handler threw, for `fabrika app dev`; without it the error is only its fixed code. */
|
|
120
|
+
readonly revealHandlerErrors?: HandlerErrorSink;
|
|
121
|
+
/** The transports of the jobs that declare `mailer`, which `boot` resolved. */
|
|
122
|
+
readonly mail: MailTransports;
|
|
123
|
+
/** The environment's signed tokens, which a job that declares `signs` signs with. */
|
|
124
|
+
readonly tokens?: SignedTokens;
|
|
125
|
+
/** `ctx.url`: absolute links to the application's own procedures; without it every link is refused. */
|
|
126
|
+
readonly url?: AppUrl;
|
|
127
|
+
}
|
|
128
|
+
export interface BoundJobs {
|
|
129
|
+
readonly jobs: ContractJobs;
|
|
130
|
+
/** The handler ids the jobs took (`jobs.<name>`), so `boot` finds no undeclared handler among them. */
|
|
131
|
+
readonly declared: readonly string[];
|
|
132
|
+
/**
|
|
133
|
+
* Kicks the contract job `name`, as a site's `ctx.kick` does after its commit: a run under the job's own key
|
|
134
|
+
* now, or the shared pending flag for the run holding the lease, which then runs once more after it succeeded. It
|
|
135
|
+
* waits for no lock, settles when that work has ended and never rejects; the schedule stays the recovery path.
|
|
136
|
+
*/
|
|
137
|
+
readonly kick: (name: string) => Promise<void>;
|
|
138
|
+
/**
|
|
139
|
+
* Aborts every kicked run in flight; a later kick starts none and only sets the pending flag, for a process that is
|
|
140
|
+
* not stopping; the schedule recovers their work.
|
|
141
|
+
*/
|
|
142
|
+
readonly stopKicks: () => void;
|
|
143
|
+
}
|
|
144
|
+
/** Binds every declared job to its handler, or adds why it cannot run to `problems`. */
|
|
145
|
+
export declare function bindJobs(input: JobBindingInput, problems: string[]): BoundJobs;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface CronSchedule {
|
|
2
|
+
readonly minutes: ReadonlySet<number>;
|
|
3
|
+
readonly hours: ReadonlySet<number>;
|
|
4
|
+
readonly daysOfMonth: ReadonlySet<number>;
|
|
5
|
+
readonly months: ReadonlySet<number>;
|
|
6
|
+
readonly daysOfWeek: ReadonlySet<number>;
|
|
7
|
+
/** Both day fields name days: a day is due when either matches (Vixie cron). */
|
|
8
|
+
readonly eitherDay: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** The schedule of a five-field expression, or `null` when it is not one this runner reads. */
|
|
11
|
+
export declare function parseCron(expression: string): CronSchedule | null;
|
|
12
|
+
/** The latest `count` due minutes at or before `at`, newest first; fewer when the schedule is that rare. */
|
|
13
|
+
export declare function previousDue(schedule: CronSchedule, at: Date, count: number): Date[];
|
|
14
|
+
/** The time between the two latest due minutes at or before `at`, or `null` when there are not two in the window. */
|
|
15
|
+
export declare function periodAt(schedule: CronSchedule, at: Date): number | null;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CapturedEnvironment } from '../implement/environment';
|
|
2
|
+
export declare const REDACTED = "[redacted]";
|
|
3
|
+
/** A shorter value is no credential the platform generates, and scrubbing it would mangle ordinary text. */
|
|
4
|
+
export declare const MIN_SECRET_LENGTH = 8;
|
|
5
|
+
export interface Scrubber {
|
|
6
|
+
scrub(text: string): string;
|
|
7
|
+
}
|
|
8
|
+
/** Every value of `environment` that is a secret, with the user and password of a URL-shaped one. */
|
|
9
|
+
export declare function secretValues(environment: CapturedEnvironment): string[];
|
|
10
|
+
export declare function secretScrubber(secrets: readonly string[]): Scrubber;
|
|
11
|
+
/** The scrubber of one application environment: its secrets' values, then the credential patterns. */
|
|
12
|
+
export declare function environmentScrubber(environment: CapturedEnvironment): Scrubber;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { EmailDeliveryResult, EmailSenderCapabilities } from '@fabrika/email';
|
|
2
|
+
import type { CronJob, DimensionOf } from '../contract/define';
|
|
3
|
+
import type { FileRef } from '../contract/file';
|
|
4
|
+
import type { AppCapability, Ref, Within } from '../implement/capability';
|
|
5
|
+
import type { AppUrl } from '../implement/urls';
|
|
6
|
+
/** Who a job's writes are stamped with (`createdBy`): the job itself, never a person. */
|
|
7
|
+
export interface JobPrincipal {
|
|
8
|
+
readonly kind: 'job';
|
|
9
|
+
/** `job:<name>`. */
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly job: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* What a job handler gets. A capability is minted for a declared action only when the token IAM signed for the job's
|
|
15
|
+
* key at the start of the attempt carries it, with the job's `scope: 'app'`, and only while this attempt holds the
|
|
16
|
+
* job's lease: a capability of another attempt is refused, and so is every write after the attempt timed out or lost
|
|
17
|
+
* the lease.
|
|
18
|
+
*/
|
|
19
|
+
export interface JobContext<A extends string, D extends string> {
|
|
20
|
+
readonly principal: JobPrincipal;
|
|
21
|
+
/** This attempt's id; it is also the holder of the job's lease. */
|
|
22
|
+
readonly runId: string;
|
|
23
|
+
/** 1 for the first attempt, one more for each retry. */
|
|
24
|
+
readonly attempt: number;
|
|
25
|
+
/** Aborted when the attempt times out or loses its lease. */
|
|
26
|
+
readonly signal: AbortSignal;
|
|
27
|
+
/** Every value of `dimension`, for a declared action: what a list or an aggregate takes. */
|
|
28
|
+
within<const Act extends A, const Dim extends D>(action: Act, dimension: Dim): Within<Dim, Act>;
|
|
29
|
+
/** One object of `dimension` for a declared action, located now: what a write takes. A missing object is NotFound. */
|
|
30
|
+
ref<const Act extends A, const Dim extends D>(action: Act, dimension: Dim, id: string): Promise<Ref<Dim, Act>>;
|
|
31
|
+
/** A declared action, application-wide: what `ctx.db.app('<action>')` reads and writes the application tables with. */
|
|
32
|
+
app<const Act extends A>(action: Act): AppCapability<Act>;
|
|
33
|
+
/** An absolute link to a procedure of this application: `ctx.url('api.mail.unsubscribe', { query: { t } })`. */
|
|
34
|
+
readonly url: AppUrl;
|
|
35
|
+
}
|
|
36
|
+
/** One message a job sends. The framework prefixes `idempotencyKey` before the transport sees it. */
|
|
37
|
+
export interface AppMailMessage {
|
|
38
|
+
readonly to: string;
|
|
39
|
+
readonly subject: string;
|
|
40
|
+
readonly text: string;
|
|
41
|
+
readonly html?: string;
|
|
42
|
+
/** 1..200 visible ASCII characters, unique per message within this application environment. */
|
|
43
|
+
readonly idempotencyKey: string;
|
|
44
|
+
readonly replyTo?: string;
|
|
45
|
+
readonly fromName?: string;
|
|
46
|
+
/**
|
|
47
|
+
* A one-click unsubscribe (RFC 8058): the transport writes `List-Unsubscribe` and `List-Unsubscribe-Post`. `url` is
|
|
48
|
+
* an `https:` link, usually `ctx.url` of a public procedure that verifies a signed token.
|
|
49
|
+
*/
|
|
50
|
+
readonly unsubscribe?: {
|
|
51
|
+
readonly url: string;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Files the message carries: references this attempt holds and may read, as `ctx.files.open` takes them. Only a
|
|
55
|
+
* `live` file is sent; the bytes of all of them together stay within the transport's `maxAttachmentBytes`, and a
|
|
56
|
+
* transport without it refuses the message. The idempotency key covers the attachments.
|
|
57
|
+
*/
|
|
58
|
+
readonly attachments?: readonly FileRef[];
|
|
59
|
+
}
|
|
60
|
+
/** `ctx.mail` of a job that declares `mailer`. */
|
|
61
|
+
export interface AppMail {
|
|
62
|
+
/**
|
|
63
|
+
* false where the installation has no mail transport (`FABRIKA_EMAIL_PROVIDER=none`, or a local one without any):
|
|
64
|
+
* every `send` fails `mail_not_configured`, outcome `not_sent`. An outbox then leaves its rows queued and spends no
|
|
65
|
+
* attempt; they leave on the first run after the operator adds a transport and the application is deployed again.
|
|
66
|
+
*/
|
|
67
|
+
readonly available: boolean;
|
|
68
|
+
/** What the transport promises: whether a repeat under one key is deduplicated, sync or queued acceptance. */
|
|
69
|
+
readonly capabilities: EmailSenderCapabilities;
|
|
70
|
+
/**
|
|
71
|
+
* Hands one message to the transport. Rejects with `EmailDeliveryError`; its `outcome` says whether the message may
|
|
72
|
+
* have left. `signal` adds to the attempt's own `ctx.signal`, which always aborts the send.
|
|
73
|
+
*/
|
|
74
|
+
send(message: AppMailMessage, signal?: AbortSignal): Promise<EmailDeliveryResult>;
|
|
75
|
+
/** `mayRetryDelivery` over this transport: true only for an `EmailDeliveryError` that is safe to send again. */
|
|
76
|
+
mayRetry(error: unknown): boolean;
|
|
77
|
+
}
|
|
78
|
+
export type JobHandler<A extends string, D extends string> = (args: {
|
|
79
|
+
readonly ctx: JobContext<A, D>;
|
|
80
|
+
}) => void | Promise<void>;
|
|
81
|
+
type JobsOf<C> = C extends {
|
|
82
|
+
readonly surface: {
|
|
83
|
+
readonly jobs: infer J;
|
|
84
|
+
};
|
|
85
|
+
} ? J : never;
|
|
86
|
+
/** `{ jobs: { <name>: handler } }` for a contract that declares jobs, and nothing for one that declares none. */
|
|
87
|
+
export type JobHandlersOf<C> = [JobsOf<C>] extends [never] ? unknown : {
|
|
88
|
+
readonly jobs: {
|
|
89
|
+
readonly [K in keyof JobsOf<C>]: JobsOf<C>[K] extends CronJob<infer A> ? JobHandler<A, DimensionOf<C>> : never;
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { PageCaller } from '../client/caller';
|
|
3
|
+
/** A scope value as the token names it: `{ type: 'org', value: 'acme' }`. */
|
|
4
|
+
export interface CallerScope {
|
|
5
|
+
readonly type: string;
|
|
6
|
+
readonly value: string;
|
|
7
|
+
}
|
|
8
|
+
export interface Caller {
|
|
9
|
+
readonly label: string;
|
|
10
|
+
readonly email: string;
|
|
11
|
+
/**
|
|
12
|
+
* Whether the token grants `action`: without `scope`, app-wide or in any scope value; with `scope`, app-wide or in
|
|
13
|
+
* that value itself. A grant on a container is not seen from a value inside it (the page knows no chain): ask at
|
|
14
|
+
* the dimension the person holds the role in.
|
|
15
|
+
*/
|
|
16
|
+
can(action: string, scope?: CallerScope): boolean;
|
|
17
|
+
}
|
|
18
|
+
/** The caller of a parsed document; exported for a page that reads the document itself. */
|
|
19
|
+
export declare function callerOf(document: PageCaller): Caller;
|
|
20
|
+
/** The page's caller, or `null` while the document loads and when it could not be read (a public SPA answers 401). */
|
|
21
|
+
export declare function useCaller(): Caller | null;
|
|
22
|
+
export interface CanProps {
|
|
23
|
+
readonly action: string;
|
|
24
|
+
readonly scope?: CallerScope;
|
|
25
|
+
/** Shown instead, also while the caller document loads; nothing by default. */
|
|
26
|
+
readonly fallback?: ReactNode;
|
|
27
|
+
readonly children?: ReactNode;
|
|
28
|
+
}
|
|
29
|
+
/** Renders its children when the caller's token grants `action` (see `Caller.can`). Never the access check. */
|
|
30
|
+
export declare function Can({ action, scope, fallback, children }: CanProps): ReactNode;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type FabrikaError = {
|
|
2
|
+
readonly kind: 'unauthenticated';
|
|
3
|
+
readonly message: string;
|
|
4
|
+
/** Where the proxy sends the person to sign in; `null` when the answer carried none. */
|
|
5
|
+
readonly loginUrl: string | null;
|
|
6
|
+
} | {
|
|
7
|
+
readonly kind: 'forbidden';
|
|
8
|
+
readonly message: string;
|
|
9
|
+
} | {
|
|
10
|
+
readonly kind: 'conflict';
|
|
11
|
+
readonly message: string;
|
|
12
|
+
} | {
|
|
13
|
+
readonly kind: 'validation';
|
|
14
|
+
readonly message: string;
|
|
15
|
+
/** The first message for each refused field, by its dotted input path (`title`, `lines.0.amount`). */
|
|
16
|
+
readonly fields: Readonly<Record<string, string>>;
|
|
17
|
+
} | {
|
|
18
|
+
readonly kind: 'not-found';
|
|
19
|
+
readonly message: string;
|
|
20
|
+
} | {
|
|
21
|
+
readonly kind: 'other';
|
|
22
|
+
readonly message: string;
|
|
23
|
+
/** The HTTP status; `0` when the request never got an answer. */
|
|
24
|
+
readonly status: number;
|
|
25
|
+
};
|
|
26
|
+
export type FabrikaErrorKind = FabrikaError['kind'];
|
|
27
|
+
interface Issue {
|
|
28
|
+
readonly path: string;
|
|
29
|
+
readonly message: string;
|
|
30
|
+
}
|
|
31
|
+
/** The first message per path: a field shows one line. */
|
|
32
|
+
export declare function fieldsOf(issues: readonly Issue[]): Readonly<Record<string, string>>;
|
|
33
|
+
/**
|
|
34
|
+
* A refused call as the union, read structurally off the client's `RpcError` (`httpStatus`, `type`, `issues`,
|
|
35
|
+
* `loginUrl`); any other thrown value (a bug in the page) is `other` with status `0`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function toFabrikaError(error: unknown): FabrikaError;
|
|
38
|
+
/** The message for one field of a validation error, or `undefined`: what a form shows under that field. */
|
|
39
|
+
export declare function fieldError(error: FabrikaError | null, path: string): string | undefined;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
import type { AttachmentProcedure, DownloadedFile, DownloadInput, FileProcedure } from '../client/contract';
|
|
3
|
+
import type { Schema } from '../contract/define';
|
|
4
|
+
import { type FabrikaError } from './errors';
|
|
5
|
+
import type { InputArgument, InvalidateTarget, PagedProcedure, PageInput, TypedProcedure } from './types';
|
|
6
|
+
/** Pass as the input to wait: the query sends nothing until its input is known. */
|
|
7
|
+
export declare const skip: unique symbol;
|
|
8
|
+
export type QueryState<T> = {
|
|
9
|
+
readonly fetching: boolean;
|
|
10
|
+
refetch(): Promise<void>;
|
|
11
|
+
} & ({
|
|
12
|
+
readonly status: 'idle';
|
|
13
|
+
readonly data: undefined;
|
|
14
|
+
readonly error: null;
|
|
15
|
+
} | {
|
|
16
|
+
readonly status: 'loading';
|
|
17
|
+
readonly data: undefined;
|
|
18
|
+
readonly error: null;
|
|
19
|
+
} | {
|
|
20
|
+
readonly status: 'success';
|
|
21
|
+
readonly data: T;
|
|
22
|
+
readonly error: null;
|
|
23
|
+
}
|
|
24
|
+
/** `data` keeps the last answer when a refetch failed. */
|
|
25
|
+
| {
|
|
26
|
+
readonly status: 'error';
|
|
27
|
+
readonly data: T | undefined;
|
|
28
|
+
readonly error: FabrikaError;
|
|
29
|
+
});
|
|
30
|
+
export interface QueryOptions {
|
|
31
|
+
/**
|
|
32
|
+
* While a new input's answer loads, keep the previous input's answer as `success` data, with `fetching` true: an
|
|
33
|
+
* offset list (`{ items, total }`) whose `limit` or `offset` changes keeps its rows on screen.
|
|
34
|
+
*/
|
|
35
|
+
readonly keepPrevious?: boolean;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The answer of a query, cached by procedure and input: every view of one call shares one request, and a mutation
|
|
39
|
+
* that invalidates it refetches it. Pass `skip` as the input to wait for a value the input needs.
|
|
40
|
+
*/
|
|
41
|
+
export declare function useQuery<O extends Schema>(procedure: TypedProcedure<Record<never, never> | undefined, O>, input?: typeof skip): QueryState<z.output<O>>;
|
|
42
|
+
export declare function useQuery<I, O extends Schema>(procedure: TypedProcedure<I, O>, input: I | typeof skip, options?: QueryOptions): QueryState<z.output<O>>;
|
|
43
|
+
export interface InfiniteQueryOptions {
|
|
44
|
+
/** How many pages the first load reads: `1` (the default) or every page (`'all'`, for a picker's options). */
|
|
45
|
+
readonly pages?: number | 'all';
|
|
46
|
+
}
|
|
47
|
+
export interface InfiniteQueryState<Item> {
|
|
48
|
+
readonly status: 'idle' | 'loading' | 'success' | 'error';
|
|
49
|
+
/** Every item of the loaded pages, in order. */
|
|
50
|
+
readonly items: readonly Item[];
|
|
51
|
+
readonly error: FabrikaError | null;
|
|
52
|
+
readonly hasMore: boolean;
|
|
53
|
+
readonly fetching: boolean;
|
|
54
|
+
readonly loadingMore: boolean;
|
|
55
|
+
loadMore(): Promise<void>;
|
|
56
|
+
refetch(): Promise<void>;
|
|
57
|
+
/** After a failed later page, asks for that page again; after a failed first load, loads again. */
|
|
58
|
+
retry(): Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A cursor list (`{ items, next }`, input `after`): the loaded pages as one list and `loadMore` for the next page. A
|
|
62
|
+
* refetch reads as many pages as are loaded again, from the first cursor.
|
|
63
|
+
*/
|
|
64
|
+
export declare function useInfiniteQuery<I extends {
|
|
65
|
+
readonly after?: string;
|
|
66
|
+
} | undefined, Item>(procedure: PagedProcedure<I, Item>, input: PageInput<I> | typeof skip, options?: InfiniteQueryOptions): InfiniteQueryState<Item>;
|
|
67
|
+
/** What `useDownload`'s `mutate` takes: the attachment procedure's input, or none. */
|
|
68
|
+
export type DownloadArgument<I> = DownloadInput<I>[0];
|
|
69
|
+
/**
|
|
70
|
+
* The download of an attachment procedure (`api.reports.export`) through the provider's client of its mount, so a test
|
|
71
|
+
* that hands the provider a client over the kit downloads over the kit. `mutate(input)` saves the file in a browser and
|
|
72
|
+
* answers it as `data`; a refusal is the state's `error`, as for `useMutation`. It refetches nothing.
|
|
73
|
+
*/
|
|
74
|
+
export declare function useDownload<I>(procedure: AttachmentProcedure<I> | FileProcedure<I>): MutationState<DownloadArgument<I>, DownloadedFile>;
|
|
75
|
+
export type MutationResult<T> = {
|
|
76
|
+
readonly ok: true;
|
|
77
|
+
readonly data: T;
|
|
78
|
+
} | {
|
|
79
|
+
readonly ok: false;
|
|
80
|
+
readonly error: FabrikaError;
|
|
81
|
+
};
|
|
82
|
+
export interface MutationState<I, T> {
|
|
83
|
+
readonly status: 'idle' | 'pending' | 'success' | 'error';
|
|
84
|
+
readonly pending: boolean;
|
|
85
|
+
readonly data: T | undefined;
|
|
86
|
+
readonly error: FabrikaError | null;
|
|
87
|
+
/** Runs the call; never throws: a refusal is `{ ok: false, error }` and the state's `error`. */
|
|
88
|
+
mutate(input: I): Promise<MutationResult<T>>;
|
|
89
|
+
reset(): void;
|
|
90
|
+
}
|
|
91
|
+
export interface MutationOptions<T> {
|
|
92
|
+
/**
|
|
93
|
+
* What to refetch after a success or a `conflict`: procedures and groups of the client. Without it the procedure's own group
|
|
94
|
+
* (`api.deals` for `api.deals.create`); `[]` refetches nothing.
|
|
95
|
+
*/
|
|
96
|
+
readonly invalidates?: readonly InvalidateTarget[];
|
|
97
|
+
readonly onSuccess?: (data: T) => void;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* A mutation of the client: `mutate(input)` checks the input with the procedure's schema (a refused input is a
|
|
101
|
+
* `validation` error and sends nothing), calls it, and after a success refetches what `invalidates` names.
|
|
102
|
+
*/
|
|
103
|
+
export declare function useMutation<I, O extends Schema>(procedure: TypedProcedure<I, O>, options?: MutationOptions<z.output<O>>): MutationState<InputArgument<I>, z.output<O>>;
|
|
104
|
+
/**
|
|
105
|
+
* A call that is no client procedure yet (a file upload until the client covers file collections), with the same
|
|
106
|
+
* state and error union as a mutation. It refetches only what `invalidates` names.
|
|
107
|
+
*/
|
|
108
|
+
export declare function useAction<I, T>(run: (input: I) => Promise<T>, options?: MutationOptions<T>): MutationState<I, T>;
|
|
109
|
+
/** Refetches every shown query under the procedures or groups given, and forgets the cached rest. */
|
|
110
|
+
export declare function useInvalidate(): (...targets: readonly InvalidateTarget[]) => Promise<void>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { type Caller, callerOf, type CallerScope, Can, type CanProps, useCaller } from './caller';
|
|
2
|
+
export { type FabrikaError, type FabrikaErrorKind, fieldError, toFabrikaError } from './errors';
|
|
3
|
+
export { type DownloadArgument, type InfiniteQueryOptions, type InfiniteQueryState, type MutationOptions, type MutationResult, type MutationState, type QueryOptions, type QueryState, skip, useAction, useDownload, useInfiniteQuery, useInvalidate, useMutation, useQuery, } from './hooks';
|
|
4
|
+
export { FabrikaProvider, type FabrikaProviderProps } from './provider';
|
|
5
|
+
export type { ClientGroup, ClientProcedure, InputArgument, InvalidateTarget, PageAnswer, PagedProcedure, PageInput, ProcedureInput, ProcedureResult, TypedProcedure, } from './types';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type PageCaller } from '../client/caller';
|
|
3
|
+
import { type ClientNode, type DownloadedFile } from '../client/contract';
|
|
4
|
+
import { type RpcFetch } from '../client/transport';
|
|
5
|
+
import { type FabrikaError } from './errors';
|
|
6
|
+
import { QueryStore, type StoreTarget } from './store';
|
|
7
|
+
export type CallerState = {
|
|
8
|
+
readonly status: 'loading';
|
|
9
|
+
} | {
|
|
10
|
+
readonly status: 'ready';
|
|
11
|
+
readonly caller: PageCaller;
|
|
12
|
+
} | {
|
|
13
|
+
readonly status: 'error';
|
|
14
|
+
readonly error: FabrikaError;
|
|
15
|
+
};
|
|
16
|
+
/** The caller document, read once per provider when a view first asks for it. */
|
|
17
|
+
export declare class CallerSource {
|
|
18
|
+
#private;
|
|
19
|
+
private readonly url;
|
|
20
|
+
private readonly fetch;
|
|
21
|
+
state: CallerState;
|
|
22
|
+
constructor(url: string, fetch: RpcFetch);
|
|
23
|
+
subscribe(listener: () => void): () => void;
|
|
24
|
+
}
|
|
25
|
+
export interface Fabrika {
|
|
26
|
+
readonly store: QueryStore;
|
|
27
|
+
readonly caller: CallerSource;
|
|
28
|
+
/** The call of `procedure` on the provider's client of its mount. */
|
|
29
|
+
target(procedure: unknown, hook: string): ResolvedProcedure;
|
|
30
|
+
/** The download of attachment `procedure` on the provider's client of its mount. */
|
|
31
|
+
attachment(procedure: unknown, hook: string): (input: unknown) => Promise<DownloadedFile>;
|
|
32
|
+
}
|
|
33
|
+
export interface ResolvedProcedure extends StoreTarget {
|
|
34
|
+
readonly node: ClientNode;
|
|
35
|
+
}
|
|
36
|
+
export interface FabrikaProviderProps {
|
|
37
|
+
/** The clients of the mounts the page calls, as `createClient` made them; read at the first render. */
|
|
38
|
+
readonly clients: readonly object[];
|
|
39
|
+
/** The prefix of the SPA mount the page is served from: the caller document is `<spa>/__fabrika-me.json`. */
|
|
40
|
+
readonly spa?: string;
|
|
41
|
+
/**
|
|
42
|
+
* What the caller document, the browser config and the error reports are fetched with; the global `fetch` by default
|
|
43
|
+
* (a test passes the kit's).
|
|
44
|
+
*/
|
|
45
|
+
readonly fetch?: RpcFetch;
|
|
46
|
+
/**
|
|
47
|
+
* Report the page's uncaught errors and unhandled rejections to Operations, through the DSN and release the mount
|
|
48
|
+
* serves at `<spa>/__fabrika-config.json`; `true` by default. Without a DSN nothing is reported.
|
|
49
|
+
*/
|
|
50
|
+
readonly reportErrors?: boolean;
|
|
51
|
+
readonly children?: ReactNode;
|
|
52
|
+
}
|
|
53
|
+
export declare function FabrikaProvider({ clients, spa, fetch: fetchGiven, reportErrors, children }: FabrikaProviderProps): ReactNode;
|
|
54
|
+
export declare function useFabrika(hook: string): Fabrika;
|
|
55
|
+
/** The prefix a mutation invalidates by default: the group it lies in (`api.deals` for `api.deals.create`). */
|
|
56
|
+
export declare function parentOf(node: ClientNode): string;
|
|
57
|
+
/** The prefix of a procedure or a group, for `invalidate`. */
|
|
58
|
+
export declare function prefixOf(target: unknown): string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { FabrikaError } from './errors';
|
|
2
|
+
/** A procedure as the store calls it: its id (`<mount>.<path>`) and the call of the provider's client. */
|
|
3
|
+
export interface StoreTarget {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly run: (input: unknown) => Promise<unknown>;
|
|
6
|
+
}
|
|
7
|
+
export type QueryStatus = 'loading' | 'success' | 'error';
|
|
8
|
+
export interface QuerySnapshot {
|
|
9
|
+
readonly status: QueryStatus;
|
|
10
|
+
readonly data: unknown;
|
|
11
|
+
readonly error: FabrikaError | null;
|
|
12
|
+
/** A request of this entry is in flight: the first load, a refetch or the next page. */
|
|
13
|
+
readonly fetching: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface PagesSnapshot {
|
|
16
|
+
readonly status: QueryStatus;
|
|
17
|
+
readonly items: readonly unknown[];
|
|
18
|
+
readonly next: string | null;
|
|
19
|
+
readonly error: FabrikaError | null;
|
|
20
|
+
readonly fetching: boolean;
|
|
21
|
+
readonly loadingMore: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** One page of a cursor list, as every list procedure of the paved road answers it. */
|
|
24
|
+
export interface Page {
|
|
25
|
+
readonly items: readonly unknown[];
|
|
26
|
+
readonly next: string | null;
|
|
27
|
+
}
|
|
28
|
+
type Listener = () => void;
|
|
29
|
+
/** JSON with sorted keys, so two inputs that are the same value are the same key. */
|
|
30
|
+
export declare function stableKey(value: unknown): string;
|
|
31
|
+
/** `<mount>.<path>` lies under `prefix` when it is the prefix itself or one of its descendants. */
|
|
32
|
+
export declare function under(id: string, prefix: string): boolean;
|
|
33
|
+
declare abstract class Entry<S> {
|
|
34
|
+
#private;
|
|
35
|
+
readonly target: StoreTarget;
|
|
36
|
+
readonly input: unknown;
|
|
37
|
+
snapshot: S;
|
|
38
|
+
protected sequence: number;
|
|
39
|
+
constructor(target: StoreTarget, input: unknown, initial: S);
|
|
40
|
+
get watched(): boolean;
|
|
41
|
+
subscribe(listener: Listener): () => void;
|
|
42
|
+
protected publish(snapshot: S): void;
|
|
43
|
+
abstract load(): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
export declare class QueryEntry extends Entry<QuerySnapshot> {
|
|
46
|
+
private readonly mapError;
|
|
47
|
+
constructor(target: StoreTarget, input: unknown, mapError: (error: unknown) => FabrikaError);
|
|
48
|
+
load(): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
export declare class PagesEntry extends Entry<PagesSnapshot> {
|
|
51
|
+
#private;
|
|
52
|
+
private readonly mapError;
|
|
53
|
+
constructor(target: StoreTarget, input: unknown, pages: number, mapError: (error: unknown) => FabrikaError);
|
|
54
|
+
/** Loads the first `#pages` pages again, from the first cursor. */
|
|
55
|
+
load(): Promise<void>;
|
|
56
|
+
/** Appends the next page; nothing while a request is in flight or when the list has no more. */
|
|
57
|
+
loadMore(): Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
export declare class QueryStore {
|
|
60
|
+
#private;
|
|
61
|
+
private readonly mapError;
|
|
62
|
+
constructor(mapError: (error: unknown) => FabrikaError);
|
|
63
|
+
query(target: StoreTarget, input: unknown): QueryEntry;
|
|
64
|
+
pages(target: StoreTarget, input: unknown, pages: number): PagesEntry;
|
|
65
|
+
/** Refetches every entry under one of `prefixes` that a view shows, and forgets the rest, so their next view loads. */
|
|
66
|
+
invalidate(prefixes: readonly string[]): Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface InstalledDom {
|
|
2
|
+
/** Closes the window and removes its globals; call it from the test file's `afterAll`. */
|
|
3
|
+
uninstall(): Promise<void>;
|
|
4
|
+
}
|
|
5
|
+
export interface DomOptions {
|
|
6
|
+
/** The page's address; `http://app.test/` by default. A router needs a real origin. */
|
|
7
|
+
readonly url?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Installs happy-dom's window as the globals, keeping Bun's fetch, abort, URL, blob, file and form classes, with
|
|
11
|
+
* navigation off: a download's link click leaves the page where it is, so a later test of the file still has it. Call
|
|
12
|
+
* it in a module the test file imports first: React reads `window` when it loads.
|
|
13
|
+
*/
|
|
14
|
+
export declare function installDom(options?: DomOptions): InstalledDom;
|
|
15
|
+
type Control = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
|
16
|
+
/** The one input, textarea or select whose accessible name is `label`; throws when there is none or more than one. */
|
|
17
|
+
export declare function controlByLabel(label: string, within?: ParentNode): Control;
|
|
18
|
+
/**
|
|
19
|
+
* Types `value` into the control labelled `label`, as React reads a person typing: it focuses the control, sets the
|
|
20
|
+
* value past React's tracking, and sends `input`, `change` and `keyup`. A select takes the option's value.
|
|
21
|
+
*/
|
|
22
|
+
export declare function type(label: string, value: string, within?: ParentNode): Promise<void>;
|
|
23
|
+
/** Waits, letting React render, until the text of `within` holds `text`; throws after `timeoutMs` (2 s by default). */
|
|
24
|
+
export declare function waitForText(text: string, options?: {
|
|
25
|
+
readonly within?: Element;
|
|
26
|
+
readonly timeoutMs?: number;
|
|
27
|
+
}): Promise<void>;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { z } from 'zod/v4';
|
|
2
|
+
import type { Schema } from '../contract/define';
|
|
3
|
+
/** A query or mutation of a client that `createClient` built. */
|
|
4
|
+
export type ClientProcedure = ((input: never) => Promise<unknown>) & {
|
|
5
|
+
readonly output: Schema;
|
|
6
|
+
};
|
|
7
|
+
/** A group of a client tree (`api.deals`), or the client itself: every procedure under it. */
|
|
8
|
+
export interface ClientGroup {
|
|
9
|
+
readonly [name: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
/** What `invalidate` takes: one procedure, or a group for every procedure under it. */
|
|
12
|
+
export type InvalidateTarget = ClientProcedure | ClientGroup;
|
|
13
|
+
/** The input as the caller writes it; `undefined` admitted for a procedure that takes none. */
|
|
14
|
+
export type ProcedureInput<F extends ClientProcedure> = F extends (input: infer I) => Promise<unknown> ? I : never;
|
|
15
|
+
/** A procedure by its input `I` and output schema `O`: what the hooks take, so both are inferred from the call. */
|
|
16
|
+
export type TypedProcedure<I, O extends Schema> = ((input: I) => Promise<unknown>) & {
|
|
17
|
+
readonly output: O;
|
|
18
|
+
};
|
|
19
|
+
/** An input argument that may be left out when the procedure takes no input. */
|
|
20
|
+
export type InputArgument<I> = undefined extends I ? I | void : I;
|
|
21
|
+
/** The answer, as the procedure's output schema parses it. */
|
|
22
|
+
export type ProcedureResult<F extends ClientProcedure> = z.output<F['output']>;
|
|
23
|
+
/** The answer of a cursor list: `{ items, next }`. */
|
|
24
|
+
export interface PageAnswer<Item> {
|
|
25
|
+
readonly items: readonly Item[];
|
|
26
|
+
readonly next: string | null;
|
|
27
|
+
}
|
|
28
|
+
/** A procedure that pages: input `after`, answer `{ items, next }`. */
|
|
29
|
+
export type PagedProcedure<I, Item> = ((input: I) => Promise<unknown>) & {
|
|
30
|
+
readonly output: z.core.$ZodType<PageAnswer<Item>>;
|
|
31
|
+
};
|
|
32
|
+
/** The input of a paged procedure without its cursor: the hook sends `after` itself. */
|
|
33
|
+
export type PageInput<I> = undefined extends I ? Omit<NonNullable<I>, 'after'> | void : Omit<I, 'after'>;
|