eraspace 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -151,8 +151,24 @@ export interface TableDefinition<F extends FieldMap = FieldMap, I extends IndexM
151
151
  name: N,
152
152
  fields: Fs,
153
153
  ): TableDefinition<F, Simplify<I & { [P in N]: Fs }>>;
154
+ /**
155
+ * Upgrade rows written before this migration to the table's current fields. Versions start at any positive
156
+ * number and each call takes the next one; rows are written at the last version. A deploy then accepts rows of
157
+ * an older shape: they are upgraded when read and rewritten in the background. `upgrade` receives the stored
158
+ * row with `id` and `createdAt` and must return the new row synchronously, depending only on the row.
159
+ * Undeclared fields of the result are dropped. Keep a migration until every row has passed it.
160
+ */
161
+ migrate(version: number, upgrade: (row: StoredRow<F>) => MigratedRow<F>): TableDefinition<F, I>;
154
162
  }
155
163
 
164
+ /**
165
+ * A row as an earlier deployment stored it: `id`, `createdAt` and whatever fields it had then. Their types are
166
+ * unknown, and fields of the current schema may be absent, so they read as `any`.
167
+ */
168
+ export type StoredRow<F extends FieldMap = FieldMap> = { id: string; createdAt: number } & { [K in keyof F]: any } & { [field: string]: any };
169
+ /** What a migration returns: the row with the table's current fields. `id` and `createdAt` keep their values. */
170
+ export type MigratedRow<F extends FieldMap> = InferInsert<F> & { id?: string; createdAt?: number };
171
+
156
172
  export type AnyTable = TableDefinition<any, any>;
157
173
  export type Schema = Record<string, AnyTable>;
158
174
  /** The schema a capsule gets when it declares none. Tables and fields are untyped. */
@@ -160,6 +176,7 @@ export type DefaultSchema = Record<string, TableDefinition<Record<string, Field<
160
176
 
161
177
  export function table<const F extends FieldMap>(fields: F): TableDefinition<F, {}> {
162
178
  const indexes: Record<string, readonly string[]> = {};
179
+ const migrations: number[] = [];
163
180
  const def: any = {
164
181
  kind: "table",
165
182
  fields,
@@ -169,10 +186,20 @@ export function table<const F extends FieldMap>(fields: F): TableDefinition<F, {
169
186
  indexes[name] = idx;
170
187
  return def;
171
188
  },
189
+ migrate(version: number, upgrade: unknown) {
190
+ if (!Number.isInteger(version) || version < 1) throw new TypeError("migrate() expects a positive integer version");
191
+ if (typeof upgrade !== "function") throw new TypeError("migrate() expects a function from a stored row to the upgraded row");
192
+ const last = migrations.length ? migrations[migrations.length - 1]! : version - 1;
193
+ if (version !== last + 1) throw new Error(`migrate(${version}) must follow migrate(${last}) with the next version, ${last + 1}`);
194
+ migrations.push(version);
195
+ return def;
196
+ },
172
197
  toJSON() {
198
+ const versions = migrations.length ? { version: migrations[migrations.length - 1], migratesFrom: migrations[0]! - 1 } : {};
173
199
  return {
174
200
  fields: Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, describeField(v)])),
175
201
  indexes,
202
+ ...versions,
176
203
  };
177
204
  },
178
205
  };
@@ -349,6 +376,16 @@ interface BaseContext {
349
376
  interface DbHolder<Db> extends BaseContext {
350
377
  db: Db;
351
378
  }
379
+ export interface Scheduler {
380
+ /**
381
+ * Runs mutation `name` with `args` once, `delayMs` milliseconds after this mutation commits. The job commits with
382
+ * this mutation, so it exists only if this mutation does, and its run commits at most once. Returns the job's id.
383
+ */
384
+ runAfter(delayMs: number, name: string, ...args: any[]): string;
385
+ }
386
+ interface MutationHolder<Db> extends DbHolder<Db> {
387
+ scheduler: Scheduler;
388
+ }
352
389
  interface ActionHolder<Db> extends DbHolder<Db> {
353
390
  runQuery(name: string, ...args: any[]): Promise<any>;
354
391
  runMutation(name: string, ...args: any[]): Promise<any>;
@@ -357,8 +394,8 @@ interface ActionHolder<Db> extends DbHolder<Db> {
357
394
 
358
395
  /** The `ctx` a `query()` (or a `readOnly` endpoint) receives. */
359
396
  export type QueryServerContext<S extends Schema = DefaultSchema> = DbHolder<DbContext<S>>;
360
- /** The `ctx` a `mutation()` or writable `endpoint()` receives. Its `db` can write. */
361
- export type ServerContext<S extends Schema = DefaultSchema> = DbHolder<MutationDbContext<S>>;
397
+ /** The `ctx` a `mutation()` or writable `endpoint()` receives. Its `db` can write and its `scheduler` schedules mutations. */
398
+ export type ServerContext<S extends Schema = DefaultSchema> = MutationHolder<MutationDbContext<S>>;
362
399
  /** The `ctx` an `action()` receives: read-only db plus `runQuery`/`runMutation`. Use global `fetch` for HTTP. */
363
400
  export type ActionServerContext<S extends Schema = DefaultSchema> = ActionHolder<DbContext<S>>;
364
401
 
@@ -477,6 +514,8 @@ export interface EndpointRequest {
477
514
  json<T = unknown>(): Promise<T>;
478
515
  /** Request body bytes as a JSON-compatible number array. */
479
516
  bytes(): Promise<number[]>;
517
+ /** A multipart/form-data or application/x-www-form-urlencoded body. Other content types reject with a TypeError. */
518
+ formData(): Promise<FormData>;
480
519
  }
481
520
 
482
521
  export interface EndpointResponseOptions {
@@ -1,7 +1,7 @@
1
1
  import { createClient, FilePreview, FileUpload, type FileRef } from "eraspace/client";
2
2
  import { useState, useEffect } from "preact/hooks";
3
3
  import type app from "../server/index";
4
- const client = createClient<typeof app>({ reloadOnDeploy: true });
4
+ const client = createClient<typeof app>();
5
5
  export default function App() {
6
6
  const [room, setRoom] = useState("general");
7
7
  const [author, setAuthor] = useState("Guest");
@@ -1,7 +1,7 @@
1
1
  import { createClient } from "eraspace/client";
2
2
  import { useState } from "preact/hooks";
3
3
  import type app from "../server/index";
4
- const client = createClient<typeof app>({ reloadOnDeploy: true });
4
+ const client = createClient<typeof app>();
5
5
  export default function App() {
6
6
  const options = client.useQuery("results");
7
7
  const add = client.useMutation("addOption");
@@ -1,7 +1,7 @@
1
1
  import { createClient } from "eraspace/client";
2
2
  import { useState } from "preact/hooks";
3
3
  import type app from "../server/index";
4
- const client = createClient<typeof app>({ reloadOnDeploy: true });
4
+ const client = createClient<typeof app>();
5
5
  export default function App() {
6
6
  const todos = client.useQuery("list");
7
7
  const [title, setTitle] = useState("");