primitive-functions 0.2.0-alpha.4 → 0.2.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/index.d.ts +375 -41
  2. package/index.js +5 -3
  3. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -23,12 +23,41 @@ export interface FunctionUser {
23
23
  email: string | null;
24
24
  }
25
25
 
26
- /** What started this invocation. */
26
+ /**
27
+ * What started this invocation. Narrow on `kind`.
28
+ *
29
+ * The same union the runtime builds (`src/server-functions/trigger-descriptor.ts`),
30
+ * held identical by `tests/api/spec/function-trigger-declarations-3281.test.ts`:
31
+ * a field the fire path sets is declared here, and nothing is declared here
32
+ * that the fire path does not set.
33
+ */
27
34
  export type FunctionTrigger =
35
+ /** An HTTP invocation: the caller is `ctx.user`. */
28
36
  | { kind: "http" }
29
- | { kind: "webhook"; [key: string]: unknown }
30
- | { kind: "cron"; [key: string]: unknown }
31
- | { kind: "database"; databaseId: string; databaseType: string; [key: string]: unknown }
37
+ /** A verified webhook delivery to the function's own key. */
38
+ | {
39
+ kind: "webhook";
40
+ /** The webhook key, which is the function key. */
41
+ webhookKey: string;
42
+ /** The platform's webhook row the delivery log lives under. */
43
+ webhookId: string;
44
+ /** The provider's event id (`X-Webhook-Event-Id`), when it sent one. */
45
+ externalEventId: string | null;
46
+ }
47
+ /** A cron entry firing, scheduled or from the `cron-triggers test` diagnostic. */
48
+ | {
49
+ kind: "cron";
50
+ /** The `[[function.triggers.cron]]` entry's `name`. */
51
+ name: string;
52
+ /** The platform's cron row. */
53
+ triggerId: string;
54
+ /** ISO 8601. The instant the fire was scheduled for; a diagnostic fire's is "now". */
55
+ scheduledFor: string;
56
+ /** Present, and true, only on the diagnostic fire: nothing was scheduled. */
57
+ manual?: true;
58
+ }
59
+ /** A write to a watched database type. */
60
+ | { kind: "database"; databaseId: string; databaseType: string }
32
61
  /**
33
62
  * A DSL workflow's `workflow.call` step reached this function (#3288).
34
63
  *
@@ -37,7 +66,15 @@ export type FunctionTrigger =
37
66
  * taking this variant with it. `workflowKey` names the PARENT workflow, and
38
67
  * `runId`/`stepId` locate the step inside its run.
39
68
  */
40
- | { kind: "workflow"; workflowKey: string; runId: string; stepId: string };
69
+ | { kind: "workflow"; workflowKey: string; runId: string; stepId: string }
70
+ /**
71
+ * Another function started this one with `ctx.functions.start` (#3280).
72
+ *
73
+ * `functionKey` names the PARENT, and `runId` its run — null when the
74
+ * parent was a request invocation over HTTP, which writes no run row, or a
75
+ * database-change fire, which writes none either.
76
+ */
77
+ | { kind: "function"; functionKey: string; runId: string | null };
41
78
 
42
79
  /** The result shape of an operation that answers in response headers. */
43
80
  export interface HeaderResult {
@@ -57,7 +94,7 @@ export interface PrimitivePlatformError extends Error {
57
94
 
58
95
  /**
59
96
  * The typed HTTP profile, generated from the platform's OpenAPI document. Its
60
- * namespaces mirror the operation ids: analytics, blobBuckets, channels, collections, configVars, connections, databaseTypeConfigs, databases, documents, email, functions, gemini, groups, integrations, iterations, llm, locks, notifications, prompts, resourceMetadata, secrets, users.
97
+ * namespaces mirror the operation ids: analytics, blobBuckets, channels, collections, configVars, connections, databaseTypeConfigs, databases, documents, email, functions, gemini, groups, integrations, llm, locks, notifications, prompts, resourceMetadata, secrets, users.
61
98
  * Methods take one options object carrying the operation's path parameters,
62
99
  * query parameters, request headers (under their wire names) and `body`.
63
100
  *
@@ -65,11 +102,13 @@ export interface PrimitivePlatformError extends Error {
65
102
  * type-checks is a method the platform publishes — and a missing path
66
103
  * parameter or a missing required header is a compile error rather than a
67
104
  * `TypeError` at invoke time. Every call runs with the app's own SYSTEM
68
- * authority (#3279): the invocation gate was the authorization. The three
69
- * namespaces the gateway does not admit (`functions`, `iterations`,
70
- * `databaseTypeConfigs`) are still declared: they exist, they answer the
71
- * gateway's structured denial, and hiding them would make a route's
72
- * availability look like a typo.
105
+ * authority (#3279): the invocation gate was the authorization. The two
106
+ * namespaces the gateway does not admit are still declared: they exist, they
107
+ * answer the gateway's structured denial, and hiding them would make a route's
108
+ * availability look like a typo. Those are `databaseTypeConfigs` whole, and
109
+ * `functions` except for `functions.start` — the nested-start door #3280
110
+ * admits by name, which `ctx.functions.start` calls. `functions.invoke`
111
+ * stays refused: same-run composition is an import.
73
112
  */
74
113
  export interface FunctionApi {
75
114
  "analytics": {
@@ -145,6 +184,10 @@ export interface FunctionApi {
145
184
  "searchUsers"(args?: {
146
185
  "q"?: string | number | boolean;
147
186
  "limit"?: string | number | boolean;
187
+ "signupDay"?: string | number | boolean;
188
+ "signupStartDay"?: string | number | boolean;
189
+ "signupEndDay"?: string | number | boolean;
190
+ "offset"?: string | number | boolean;
148
191
  "current_timestamp"?: string | number | boolean;
149
192
  "_bust"?: string | number | boolean;
150
193
  }): Promise<any>;
@@ -600,7 +643,7 @@ export interface FunctionApi {
600
643
  "query"(args: {
601
644
  "databaseId": string | number;
602
645
  body: unknown;
603
- }): Promise<any>;
646
+ }): Promise<{ items: any[]; hasMore: boolean; nextCursor?: string; prevCursor?: string }>;
604
647
  /** `databases.records.registerIndex` — POST /databases/{databaseId}/records/index/register. Database records: index/register (requires manager-or-higher on the database, or an app-admin/owner role) */
605
648
  "registerIndex"(args: {
606
649
  "databaseId": string | number;
@@ -968,11 +1011,22 @@ export interface FunctionApi {
968
1011
  }): Promise<any>;
969
1012
  };
970
1013
  "functions": {
971
- /** `functions.invoke` — POST /functions/{functionKey}. Invoke a server function: a synchronous one returns its result, a durable one starts a run */
1014
+ /** `functions.invoke` — POST /functions/{functionKey}. Invoke a server function: a request function returns its result, a task function starts a run */
972
1015
  "invoke"(args: {
973
1016
  "functionKey": string | number;
974
1017
  body?: unknown;
975
1018
  }): Promise<any>;
1019
+ /** `functions.logs` — GET /functions/{functionKey}/logs. List a server function's invocation logs, newest first */
1020
+ "logs"(args: {
1021
+ "functionKey": string | number;
1022
+ "limit"?: string | number | boolean;
1023
+ "cursor"?: string | number | boolean;
1024
+ }): Promise<any>;
1025
+ /** `functions.start` — POST /functions/{functionKey}/start. Start a task function from inside another server function */
1026
+ "start"(args: {
1027
+ "functionKey": string | number;
1028
+ body?: unknown;
1029
+ }): Promise<any>;
976
1030
  };
977
1031
  "gemini": {
978
1032
  /** `gemini.countTokens` — POST /gemini/count-tokens. Count tokens for a Gemini request */
@@ -1080,18 +1134,6 @@ export interface FunctionApi {
1080
1134
  "integrationKey": string | number;
1081
1135
  }): Promise<any>;
1082
1136
  };
1083
- "iterations": {
1084
- /** `iterations.get` — GET /iterations/{iterationName}. Get details for a single iteration */
1085
- "get"(args: {
1086
- "iterationName": string | number;
1087
- }): Promise<any>;
1088
- /** `iterations.list` — GET /iterations. List iteration runs */
1089
- "list"(args?: Record<string, never>): Promise<any>;
1090
- /** `iterations.reset` — POST /iterations/{iterationName}/reset. Reset a terminal iteration so the next trigger runs fresh */
1091
- "reset"(args: {
1092
- "iterationName": string | number;
1093
- }): Promise<any>;
1094
- };
1095
1137
  "llm": {
1096
1138
  /** `llm.chat` — POST /llm/chat. Run an LLM chat completion */
1097
1139
  "chat"(args: {
@@ -1267,6 +1309,46 @@ export type DatabaseModels<K extends string> = K extends keyof DatabaseSchemas
1267
1309
  ? DatabaseSchemas[K]
1268
1310
  : Record<string, Record<string, unknown>>;
1269
1311
 
1312
+ /**
1313
+ * The app's functions, key by key — EMPTY here and filled by the declaration
1314
+ * `primitive config push` writes into the config tree's `functions/`
1315
+ * directory (`primitive-function-types.d.ts`): each key's declared input and
1316
+ * output types (from its `inputSchema` and `outputSchema`) and its mode. It
1317
+ * is what types `defineFunction("<key>", handler)`.
1318
+ */
1319
+ export interface FunctionSchemas {}
1320
+
1321
+ /**
1322
+ * The keys `defineFunction(key, handler)` accepts: every key the augmentation
1323
+ * declares once a tree has pushed, so a key the tree does not declare is a
1324
+ * compile error — and `string` before that, the `DatabaseTypeKeys` rule, so an
1325
+ * unaugmented tree keeps compiling.
1326
+ */
1327
+ export type FunctionKeys = [keyof FunctionSchemas] extends [never]
1328
+ ? string
1329
+ : keyof FunctionSchemas & string;
1330
+
1331
+ /** A declared function's input type, or `any` when nothing is declared. */
1332
+ export type FunctionInput<K extends string> = K extends keyof FunctionSchemas
1333
+ ? FunctionSchemas[K] extends { input: infer I }
1334
+ ? I
1335
+ : any
1336
+ : any;
1337
+
1338
+ /** A declared function's output type, or `any` when nothing is declared. */
1339
+ export type FunctionOutput<K extends string> = K extends keyof FunctionSchemas
1340
+ ? FunctionSchemas[K] extends { output: infer O }
1341
+ ? O
1342
+ : any
1343
+ : any;
1344
+
1345
+ /** A declared function's mode, fixed at generation time. */
1346
+ export type FunctionMode<K extends string> = K extends keyof FunctionSchemas
1347
+ ? FunctionSchemas[K] extends { mode: infer M }
1348
+ ? M
1349
+ : "request" | "task"
1350
+ : "request" | "task";
1351
+
1270
1352
  /**
1271
1353
  * The model names one database type addresses — its own, or any name at all
1272
1354
  * for a type that declares no schema (the DO is schemaless, so a grant on such
@@ -1311,7 +1393,14 @@ export type ModelBatchOperation<TRow> = {
1311
1393
  };
1312
1394
 
1313
1395
  export interface DatabaseModelHandle<TRow> {
1314
- query(body?: ModelScopedBody): Promise<{ data: TRow[]; hasMore?: boolean; nextCursor?: string }>;
1396
+ /**
1397
+ * One page of this model's rows, in the decided envelope — `{ items,
1398
+ * hasMore, nextCursor? }` (#3287), the same shape `ctx.doc(...)`,
1399
+ * `ctx.users.list` and the client library answer. The route's own wire
1400
+ * shape is unchanged; the SDK is where the one name lives, so shared paging
1401
+ * code can be written once.
1402
+ */
1403
+ query(body?: ModelScopedBody): Promise<{ items: TRow[]; hasMore: boolean; nextCursor?: string }>;
1315
1404
  count(body?: ModelScopedBody): Promise<any>;
1316
1405
  aggregate(body: ModelScopedBody): Promise<any>;
1317
1406
  save(body: ModelScopedBody<{ id?: string; data: Partial<TRow>; [key: string]: unknown }>): Promise<any>;
@@ -1343,6 +1432,50 @@ export interface DatabaseHandle<TModels> {
1343
1432
  runOperation(name: string, params?: Record<string, unknown>): Promise<any>;
1344
1433
  }
1345
1434
 
1435
+ /**
1436
+ * The app's DOCUMENT models — EMPTY here and filled by the declaration
1437
+ * `primitive config push` writes into the config tree's `functions/` directory
1438
+ * (`primitive-document-types.d.ts`, from the project's `models/models.toml`).
1439
+ * A project with no document schema gets the open form and still compiles.
1440
+ */
1441
+ export interface DocumentSchemas {}
1442
+
1443
+ /** The model names `ctx.doc(id).model()` accepts — every declared one, or `string` before a push. */
1444
+ export type DocumentModelNames = [keyof DocumentSchemas] extends [never]
1445
+ ? string
1446
+ : keyof DocumentSchemas & string;
1447
+
1448
+ /** A document model's row shape, or an open record for an undeclared schema. */
1449
+ export type DocumentModelRow<M extends string> = M extends keyof DocumentSchemas
1450
+ ? DocumentSchemas[M]
1451
+ : Record<string, unknown>;
1452
+
1453
+ /** The options a document model read takes: a filter object (or its JSON), a page size, a cursor. */
1454
+ export interface DocumentQueryOptions {
1455
+ filter?: Record<string, unknown> | string;
1456
+ limit?: number;
1457
+ cursor?: string;
1458
+ }
1459
+
1460
+ /**
1461
+ * One document model, bound to its document and its model name — a thin
1462
+ * binding over `ctx.api.documents.records.*` with the route's own shapes.
1463
+ */
1464
+ export interface DocumentModelHandle<TRow> {
1465
+ query(options?: DocumentQueryOptions): Promise<{ items: TRow[]; hasMore: boolean; nextCursor?: string }>;
1466
+ count(options?: { filter?: Record<string, unknown> | string }): Promise<{ count: number }>;
1467
+ aggregate(body: Record<string, unknown>): Promise<any>;
1468
+ save(body: { id: string; data: Partial<TRow>; options?: Record<string, unknown> }): Promise<{ record: TRow }>;
1469
+ patch(recordId: string, body: { data: Partial<TRow>; options?: Record<string, unknown> }): Promise<{ record: TRow }>;
1470
+ delete(recordId: string): Promise<{ deleted: boolean }>;
1471
+ }
1472
+
1473
+ /** A document, addressed once; its models are typed from `DocumentSchemas`. */
1474
+ export interface DocumentHandle {
1475
+ documentId: string;
1476
+ model<M extends DocumentModelNames>(modelName: M): DocumentModelHandle<DocumentModelRow<M>>;
1477
+ }
1478
+
1346
1479
  /**
1347
1480
  * One call through a granted integration — #3183.
1348
1481
  *
@@ -1421,6 +1554,67 @@ export interface ChannelGrantResult {
1421
1554
  expiresAt: number;
1422
1555
  }
1423
1556
 
1557
+ /** Where to start and how big a page to take — #3280. */
1558
+ export interface UserPageOptions {
1559
+ /** Page size. Capped by the route at 100. */
1560
+ limit?: number;
1561
+ /** The `nextCursor` of the page before this one. */
1562
+ cursor?: string;
1563
+ }
1564
+
1565
+ /** One user on the roster, as the platform's own users route reports them. */
1566
+ export interface UserPageItem {
1567
+ userId: string;
1568
+ email: string | null;
1569
+ name: string | null;
1570
+ avatarUrl: string | null;
1571
+ role: string;
1572
+ addedAt: string | null;
1573
+ status: string;
1574
+ disabledAt: string | null;
1575
+ [key: string]: unknown;
1576
+ }
1577
+
1578
+ /** The decided paged envelope, the same shape every paged read answers. */
1579
+ export interface UserPage {
1580
+ items: UserPageItem[];
1581
+ hasMore: boolean;
1582
+ /** Absent on the last page. */
1583
+ nextCursor?: string;
1584
+ }
1585
+
1586
+ /** How a nested start is keyed and labelled — #3280. */
1587
+ export interface FunctionStartOptions {
1588
+ /**
1589
+ * Idempotency key, scoped to the tree's root initiator and the resolved
1590
+ * context document: a repeat replays the run that already exists rather than
1591
+ * starting a second. At most 100 characters.
1592
+ */
1593
+ runKey?: string;
1594
+ /**
1595
+ * The document the child's run is keyed under. Defaults through the
1596
+ * inheritance rule: the parent run's context, else the root caller's root
1597
+ * document, else `fn:<the root function's id>`. At most 100 characters.
1598
+ */
1599
+ contextDocId?: string;
1600
+ /** Caller metadata, at most 1 KB encoded. */
1601
+ meta?: Record<string, unknown>;
1602
+ }
1603
+
1604
+ /** The start envelope, the same one a workflow start answers with — #3280. */
1605
+ export interface FunctionStartResult {
1606
+ /** Poll this on the platform's run-status route. */
1607
+ runId: string;
1608
+ runKey: string;
1609
+ instanceId: string;
1610
+ status: string;
1611
+ /**
1612
+ * True when `runKey` already named a run in this tree, which this envelope
1613
+ * describes. That is what makes a run key a singleton.
1614
+ */
1615
+ existing?: boolean;
1616
+ }
1617
+
1424
1618
  export interface FunctionContext {
1425
1619
  /** The HTTP caller, or null for a webhook, cron or database-change fire. */
1426
1620
  user: FunctionUser | null;
@@ -1435,6 +1629,13 @@ export interface FunctionContext {
1435
1629
  databaseId: string,
1436
1630
  databaseType: K
1437
1631
  ): DatabaseHandle<DatabaseModels<K>>;
1632
+ /**
1633
+ * A typed handle over one document's record models, typed from the
1634
+ * project's `models/models.toml` — `ctx.db`'s shape for documents. It
1635
+ * confers nothing: every call is a `documents.records.*` operation on the
1636
+ * same authority.
1637
+ */
1638
+ doc(documentId: string): DocumentHandle;
1438
1639
  /**
1439
1640
  * Outbound HTTP, through a granted integration and nowhere else (#3183).
1440
1641
  *
@@ -1451,6 +1652,26 @@ export interface FunctionContext {
1451
1652
  request?: IntegrationCallRequest
1452
1653
  ): Promise<IntegrationCallResult>;
1453
1654
  };
1655
+ /**
1656
+ * Start a TASK function from inside this one — #3280.
1657
+ *
1658
+ * The one nested door, and it is a START. A request function is code and
1659
+ * code composes by being IMPORTED, so there is no synchronous nested
1660
+ * invocation to reach for; a request-mode callee answers
1661
+ * `FUNCTION_MODE_MISMATCH`.
1662
+ *
1663
+ * The callee's `access` gate is not consulted — this function's own gate was
1664
+ * the authorization — and every run in the tree is keyed by whoever started
1665
+ * it, so the member who invoked the root polls and terminates them all.
1666
+ * Nesting is bounded at four levels.
1667
+ */
1668
+ functions: {
1669
+ start(
1670
+ functionKey: string,
1671
+ input?: unknown,
1672
+ options?: FunctionStartOptions
1673
+ ): Promise<FunctionStartResult>;
1674
+ };
1454
1675
  /**
1455
1676
  * Run one of the app's prompts, under a `prompt:<key>` grant (#3183).
1456
1677
  *
@@ -1485,6 +1706,24 @@ export interface FunctionContext {
1485
1706
  * error.
1486
1707
  */
1487
1708
  users: {
1709
+ /**
1710
+ * One page of the app's users — #3280.
1711
+ *
1712
+ * On the APP's system authority, whoever invoked the function: a member
1713
+ * who passes this function's gate reads whatever roster it chooses to
1714
+ * expose, so say who may in the function's own `access` expression.
1715
+ * `limit` is capped by the route at 100.
1716
+ */
1717
+ list(options?: UserPageOptions): Promise<UserPage>;
1718
+ /**
1719
+ * Every page of the app's users, in order — #3280.
1720
+ *
1721
+ * FOR A REQUEST FUNCTION AND A SINGLE-SLICE LOOP. A durable loop must take
1722
+ * its pages inside `step.do` with an EXPLICIT cursor: a memoized step does
1723
+ * not advance an iterator on resume, so an iteration across a sleep would
1724
+ * replay pages it had already taken.
1725
+ */
1726
+ iterate(options?: UserPageOptions): AsyncIterable<UserPage>;
1488
1727
  send(userId: string, payload: unknown): Promise<SendResult>;
1489
1728
  };
1490
1729
  /** The same frame to ONE connection, under a `connections:send` grant. */
@@ -1537,29 +1776,49 @@ export interface FunctionStep {
1537
1776
  export declare const SDK_STUB_VERSION: string;
1538
1777
 
1539
1778
  /**
1540
- * Declare a server function's handler.
1541
- *
1542
- * Both authoring styles reach the platform bootstrap identically — a plain
1543
- * default export taking `(input, ctx, step)` runs exactly as a wrapped one
1544
- * does — so this is a typing and intent marker, not a different calling
1545
- * convention.
1779
+ * The handler a function exports, typed for one declared key.
1546
1780
  *
1547
- * `step` is the third argument in BOTH modes (#3186). A durable run gets the
1548
- * engine's live `WorkflowStep`; a synchronous one gets a passthrough shim
1781
+ * `step` is the third argument in BOTH modes (#3186). A task run gets the
1782
+ * engine's live `WorkflowStep`; a request invocation gets a passthrough shim
1549
1783
  * (`do` runs the body, `sleep` resolves at once, `waitForEvent` throws a
1550
- * named error), so a durable-authored body can be exercised synchronously.
1784
+ * named error), so a task-authored body can be exercised as a request.
1551
1785
  */
1786
+ export type FunctionHandler<TInput, TOutput> = (
1787
+ input: TInput,
1788
+ ctx: FunctionContext,
1789
+ step: FunctionStep
1790
+ ) => Promise<TOutput>;
1791
+
1792
+ /**
1793
+ * Declare a server function's handler, typed from its declaration.
1794
+ *
1795
+ * The KEYED form, `defineFunction("<key>", handler)`, is typed from the
1796
+ * `FunctionSchemas` augmentation `primitive config push` writes beside the
1797
+ * sources: `input` is the key's declared input, the return is checked against
1798
+ * its declared output, and a key the tree does not declare is a compile error.
1799
+ * `config push` also refuses an entry whose key names another file's. In a
1800
+ * tree that has not pushed yet the key is any string and both types are `any`.
1801
+ *
1802
+ * The UNKEYED form is what it always was: author-declared generics. Both
1803
+ * styles reach the platform bootstrap identically — a plain default export
1804
+ * taking `(input, ctx, step)` runs exactly as a wrapped one does — so this is
1805
+ * a typing and intent marker, not a different calling convention.
1806
+ */
1807
+ export declare function defineFunction<K extends FunctionKeys>(
1808
+ key: K,
1809
+ handler: (
1810
+ input: FunctionInput<K>,
1811
+ ctx: FunctionContext,
1812
+ step: FunctionStep
1813
+ ) => FunctionOutput<K> | Promise<FunctionOutput<K>>
1814
+ ): FunctionHandler<FunctionInput<K>, FunctionOutput<K>> & { functionKey: K };
1552
1815
  export declare function defineFunction<TInput = unknown, TOutput = unknown>(
1553
1816
  handler: (
1554
1817
  input: TInput,
1555
1818
  ctx: FunctionContext,
1556
1819
  step: FunctionStep
1557
1820
  ) => TOutput | Promise<TOutput>
1558
- ): (
1559
- input: TInput,
1560
- ctx: FunctionContext,
1561
- step: FunctionStep
1562
- ) => Promise<TOutput>;
1821
+ ): FunctionHandler<TInput, TOutput>;
1563
1822
 
1564
1823
  /**
1565
1824
  * One declared parameter of a registered query.
@@ -1568,13 +1827,64 @@ export declare function defineFunction<TInput = unknown, TOutput = unknown>(
1568
1827
  * the platform injects the invocation's user and refuses a call that supplies
1569
1828
  * the parameter itself.
1570
1829
  */
1830
+ /** The scalar parameter types, and an array parameter's element types. */
1831
+ export type QueryScalarParamType = "string" | "number" | "boolean" | "any";
1832
+
1571
1833
  export interface QueryParamSpec {
1572
- type?: "string" | "number" | "boolean" | "any" | "$caller";
1834
+ type?: QueryScalarParamType | "$caller" | "array";
1835
+ /** For `type: "array"` — the element type; absent means any element. */
1836
+ items?: { type: QueryScalarParamType };
1573
1837
  caller?: boolean;
1574
1838
  optional?: boolean;
1575
1839
  default?: unknown;
1576
1840
  }
1577
1841
 
1842
+ /** A scalar parameter type's TypeScript type. */
1843
+ export type QueryScalarValue<T> = T extends "string"
1844
+ ? string
1845
+ : T extends "number"
1846
+ ? number
1847
+ : T extends "boolean"
1848
+ ? boolean
1849
+ : T extends "$caller"
1850
+ ? string
1851
+ : any;
1852
+
1853
+ /** The value `run` receives for one declared parameter. */
1854
+ export type QueryParamValue<S> = S extends { caller: true }
1855
+ ? string
1856
+ : S extends { type: "array"; items: { type: infer I } }
1857
+ ? QueryScalarValue<I>[]
1858
+ : S extends { type: "array" }
1859
+ ? unknown[]
1860
+ : S extends { type: infer T }
1861
+ ? QueryScalarValue<T>
1862
+ : any;
1863
+
1864
+ type QueryOptionalKeys<P> = {
1865
+ [K in keyof P]: P[K] extends { optional: true } ? K : never;
1866
+ }[keyof P];
1867
+
1868
+ /**
1869
+ * `run`'s `params`, typed from the declaration: scalars as their scalar,
1870
+ * `$caller` as a string, an array as `T[]`, an `optional: true` parameter
1871
+ * as an optional key (a parameter with a default is always present). An
1872
+ * undeclared name is a compile error.
1873
+ */
1874
+ export type QueryParams<P> = {
1875
+ [K in Exclude<keyof P, QueryOptionalKeys<P>>]: QueryParamValue<P[K]>;
1876
+ } & {
1877
+ [K in QueryOptionalKeys<P>]?: QueryParamValue<P[K]>;
1878
+ };
1879
+
1880
+ /** A registration's definition with its parameters DECLARED, so `run` is typed from them. */
1881
+ export interface TypedQueryDefinition<TDb, TSpec extends Record<string, QueryParamSpec>, TResult> {
1882
+ models: string[];
1883
+ params?: TSpec;
1884
+ cache?: { ttlMs: number };
1885
+ run: (db: TDb, params: QueryParams<TSpec>) => TResult | Promise<TResult>;
1886
+ }
1887
+
1578
1888
  export interface QueryDefinition<TDb, TParams, TResult> {
1579
1889
  /** The models this query touches. Caching is off unless the run matches. */
1580
1890
  models: string[];
@@ -1599,7 +1909,23 @@ export interface QueryDefinition<TDb, TParams, TResult> {
1599
1909
  *
1600
1910
  * Register at MODULE scope: a registration made inside the handler still works
1601
1911
  * for direct calls but is invisible to `config push`'s manifest.
1912
+ *
1913
+ * `run`'s `params` are TYPED from the declaration (the first overload,
1914
+ * inferred with a `const` type parameter): a scalar as its scalar, `$caller`
1915
+ * as a string, `{ type: "array", items: { type: "string" } }` as `string[]`,
1916
+ * an `optional: true` parameter as an optional key; an undeclared name inside
1917
+ * `run` is a compile error. A declared default is coerced to the declared
1918
+ * type at registration, so `run` receives what the declaration promises. The
1919
+ * second overload keeps an explicit `TParams`.
1602
1920
  */
1921
+ export declare function defineQuery<
1922
+ const TSpec extends Record<string, QueryParamSpec>,
1923
+ TDb = DatabaseHandle<any>,
1924
+ TResult = any
1925
+ >(
1926
+ name: string,
1927
+ definition: TypedQueryDefinition<TDb, TSpec, TResult>
1928
+ ): (db: TDb, params?: Record<string, unknown>) => Promise<TResult>;
1603
1929
  export declare function defineQuery<
1604
1930
  TDb = DatabaseHandle<any>,
1605
1931
  TParams = Record<string, any>,
@@ -1610,6 +1936,14 @@ export declare function defineQuery<
1610
1936
  ): (db: TDb, params?: Record<string, unknown>) => Promise<TResult>;
1611
1937
 
1612
1938
  /** The same, for a registration that writes. A mutation is never cached. */
1939
+ export declare function defineMutation<
1940
+ const TSpec extends Record<string, QueryParamSpec>,
1941
+ TDb = DatabaseHandle<any>,
1942
+ TResult = any
1943
+ >(
1944
+ name: string,
1945
+ definition: TypedQueryDefinition<TDb, TSpec, TResult>
1946
+ ): (db: TDb, params?: Record<string, unknown>) => Promise<TResult>;
1613
1947
  export declare function defineMutation<
1614
1948
  TDb = DatabaseHandle<any>,
1615
1949
  TParams = Record<string, any>,
package/index.js CHANGED
@@ -24,10 +24,12 @@ const NOT_PROVIDED =
24
24
  export const SDK_STUB_VERSION = "0.0.1";
25
25
 
26
26
  /**
27
- * Declare a function's handler. The platform copy returns the handler wrapped
28
- * with its invocation context; this one refuses, loudly.
27
+ * Declare a function's handler — `defineFunction(handler)` or the keyed
28
+ * `defineFunction("<key>", handler)`. The platform copy returns the handler
29
+ * wrapped with its invocation context; this one refuses, loudly, whichever
30
+ * shape it is called with.
29
31
  */
30
- export function defineFunction() {
32
+ export function defineFunction(_keyOrHandler, _maybeHandler) {
31
33
  throw new Error(NOT_PROVIDED);
32
34
  }
33
35
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primitive-functions",
3
- "version": "0.2.0-alpha.4",
3
+ "version": "0.2.0-alpha.5",
4
4
  "description": "The module a Primitive server function imports. Supplied by the platform at invoke time; reserved here so the name is ours before the runtime lands.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "Primitive LLC",