@samebase/alchemy-convex 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/State.ts CHANGED
@@ -23,13 +23,19 @@
23
23
  // The run whose project is not the oldest deletes its own, before it writes
24
24
  // anything, and uses the oldest: both runs end on one store.
25
25
  //
26
- // The function names are a wire contract: Samebase reads `stacks` and
27
- // `getOutput` by name, and an older client calls an older set of names. A
28
- // name is never renamed or removed. STORE_VERSION rises when a function
29
- // changes in a way that an older client cannot use, and a client never
30
- // pushes an older store over a newer one.
26
+ // The function names are a wire contract: Samebase reads `stacks`,
27
+ // `getOutput`, and `readStack` by name, and an older client calls an older
28
+ // set of names. A name is never renamed or removed. STORE_VERSION rises when
29
+ // a function is added or changes in a way that an older client cannot use,
30
+ // and a client never pushes an older store over a newer one.
31
+ //
32
+ // The stage output that `setOutput` stores is the stack's output plus the
33
+ // run record of src/RunRecord.ts under the key `samebase.run`: the
34
+ // repository, stack file, branch, pull request, commit, and run that wrote
35
+ // the stage. `getOutput` gives the document back as stored.
31
36
  import { dirname, join } from "node:path";
32
37
  import { fileURLToPath } from "node:url";
38
+ import * as Clock from "effect/Clock";
33
39
  import * as Effect from "effect/Effect";
34
40
  import * as Layer from "effect/Layer";
35
41
  import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
@@ -70,6 +76,7 @@ import {
70
76
  PUSH_KEY_ACTIONS,
71
77
  withPushKey,
72
78
  } from "./Push.ts";
79
+ import { type RunRecord, runRecord, withRunRecord } from "./RunRecord.ts";
73
80
  import { STORE_VERSION } from "./store/convex/version.ts";
74
81
 
75
82
  /** The name of the state project in each team. */
@@ -241,6 +248,8 @@ export interface StoreOptions {
241
248
  readonly scope: Scope.Scope;
242
249
  /** The functions directory that an outdated store gets: {@link storeDirectory}. */
243
250
  readonly store: string;
251
+ /** The record of this run, which `setOutput` adds to the stage output with the time of the write, or undefined for a run without one. */
252
+ readonly run: RunRecord | undefined;
244
253
  }
245
254
 
246
255
  /**
@@ -261,6 +270,7 @@ const pushStore = (
261
270
  push(options.spawner, key, {
262
271
  deployment: deployment.name,
263
272
  url: deployment.deploymentUrl,
273
+ rootDir: join(options.store, ".."),
264
274
  source: {
265
275
  path: options.store,
266
276
  command: [...cli, "deploy", "--typecheck", "disable", "--codegen", "disable"],
@@ -385,6 +395,21 @@ export const makeConvexState = (options: StoreOptions) =>
385
395
  }
386
396
 
387
397
  const remote = remoteOf(functions);
398
+ /** The stack's output plus the record, when the run has one and the output has a place for it. */
399
+ const documentOf = (request: { stack: string; stage: string; value: unknown }) =>
400
+ Effect.gen(function* () {
401
+ const { run } = options;
402
+ if (run === undefined) return request.value;
403
+ const recordedAt = new Date(yield* Clock.currentTimeMillis).toISOString();
404
+ const document = withRunRecord(request.value, { ...run, recordedAt });
405
+ if (document === undefined) {
406
+ yield* Effect.logWarning(
407
+ `Convex state: the output of ${request.stack}/${request.stage} is not a plain object, so it carries no run record.`,
408
+ );
409
+ return request.value;
410
+ }
411
+ return document;
412
+ });
388
413
  const service: StateService = {
389
414
  id: "convex",
390
415
  getVersion: () => Effect.succeed(STATE_STORE_VERSION),
@@ -397,7 +422,13 @@ export const makeConvexState = (options: StoreOptions) =>
397
422
  set: (request) => remote.set(request).pipe(Effect.as(request.value)),
398
423
  delete: (request) => remote.delete(request).pipe(Effect.asVoid),
399
424
  deleteStack: (request) => remote.deleteStack(request).pipe(Effect.asVoid),
400
- setOutput: (request) => remote.setOutput(request).pipe(Effect.as(request.value)),
425
+ // The document is the stack's output plus the record under `samebase.run`.
426
+ setOutput: (request) =>
427
+ Effect.gen(function* () {
428
+ const value = yield* documentOf(request);
429
+ yield* remote.setOutput({ stack: request.stack, stage: request.stage, value });
430
+ return value;
431
+ }),
401
432
  };
402
433
  return service;
403
434
  });
@@ -425,12 +456,25 @@ export const state = (
425
456
  const api = yield* ManagementApi.pipe(
426
457
  Effect.provide(ManagementApiLive().pipe(Layer.provide(fromConfig()))),
427
458
  );
459
+ // The record is derived once, before the first read. A checkout
460
+ // that gives none, such as a tarball without git, a checkout
461
+ // without origin, or a detached HEAD, still reads, plans, and
462
+ // destroys: its outputs are written without the record, and the
463
+ // log says why.
464
+ const run = yield* runRecord({ cwd: process.cwd(), env: process.env }).pipe(
465
+ Effect.catch((error) =>
466
+ Effect.logWarning(
467
+ `Convex state: the stage outputs of this run carry no record. ${error.message}`,
468
+ ).pipe(Effect.as(undefined)),
469
+ ),
470
+ );
428
471
  return yield* makeConvexState({
429
472
  team: options.team,
430
473
  api,
431
474
  spawner,
432
475
  scope,
433
476
  store: storeDirectory(),
477
+ run,
434
478
  });
435
479
  }).pipe(Effect.provideContext(context), Effect.orDie),
436
480
  );
package/src/index.ts CHANGED
@@ -8,10 +8,12 @@ export {
8
8
  fromValues,
9
9
  } from "./Credentials.ts";
10
10
  export {
11
+ AbsoluteRootDir,
11
12
  AmbiguousDeployment,
12
13
  Deployment,
13
14
  type DeploymentAttributes,
14
15
  DeploymentIdentityChange,
16
+ type DeploymentInput,
15
17
  type DeploymentProps,
16
18
  ProductionDeploymentNotFound,
17
19
  } from "./Deployment.ts";
@@ -28,6 +30,13 @@ export {
28
30
  ProjectTeamChange,
29
31
  } from "./Project.ts";
30
32
  export { Providers, providers } from "./Providers.ts";
33
+ export {
34
+ RUN_RECORD_KEY,
35
+ type RunRecord,
36
+ RunRecordError,
37
+ runRecord,
38
+ withRunRecord,
39
+ } from "./RunRecord.ts";
31
40
  export { state, STORE_PROJECT_NAME } from "./State.ts";
32
41
  export {
33
42
  CodePushFailed,
@@ -7,8 +7,8 @@
7
7
  //
8
8
  // A stage holds one row for each resource of its stack, so the reads of one
9
9
  // stage collect all rows. The function names are a wire contract: Samebase
10
- // reads `stacks` and `getOutput` by name, and an older client calls an older
11
- // set of names. Add functions; never rename or remove one.
10
+ // reads `stacks`, `getOutput`, and `readStack` by name, and an older client
11
+ // calls an older set of names. Add functions; never rename or remove one.
12
12
  import { v } from "convex/values";
13
13
  import type { QueryCtx } from "./_generated/server.js";
14
14
  import { internalMutation, internalQuery } from "./_generated/server.js";
@@ -217,3 +217,49 @@ export const setOutput = internalMutation({
217
217
  return null;
218
218
  },
219
219
  });
220
+
221
+ /**
222
+ * One call for a reader: every stage of a stack in name order, each with its
223
+ * resources in fqn order and its output, as the JSON texts that the rows
224
+ * hold. A stage with an output and no resource is listed with an empty
225
+ * resource list; a stage with resources and no output with `output: null`.
226
+ * One function reads at most 16 MiB (Convex limit): a stack whose rows go
227
+ * beyond that fails here and is read with `stages`, `list`, `get`, and
228
+ * `getOutput` instead. Since store version 2.
229
+ */
230
+ export const readStack = internalQuery({
231
+ args: { stack: v.string() },
232
+ returns: v.object({
233
+ stages: v.array(
234
+ v.object({
235
+ stage: v.string(),
236
+ resources: v.array(v.object({ fqn: v.string(), value: v.string() })),
237
+ output: v.union(v.string(), v.null()),
238
+ }),
239
+ ),
240
+ }),
241
+ handler: async (ctx, args) => {
242
+ // Both indexes order by stack, then stage, then (for resources) fqn.
243
+ const resources = await ctx.db
244
+ .query("resources")
245
+ .withIndex("by_fqn", (q) => q.eq("stack", args.stack))
246
+ .collect();
247
+ const outputs = await ctx.db
248
+ .query("outputs")
249
+ .withIndex("by_stage", (q) => q.eq("stack", args.stack))
250
+ .collect();
251
+ const stages = union(
252
+ [...new Set(resources.map((row) => row.stage))],
253
+ [...new Set(outputs.map((row) => row.stage))],
254
+ );
255
+ return {
256
+ stages: stages.map((stage) => ({
257
+ stage,
258
+ resources: resources
259
+ .filter((row) => row.stage === stage)
260
+ .map((row) => ({ fqn: row.fqn, value: row.value })),
261
+ output: outputs.find((row) => row.stage === stage)?.value ?? null,
262
+ })),
263
+ };
264
+ },
265
+ });
@@ -1,6 +1,10 @@
1
1
  // The wire version of the store: the `state:version` query answers it, and
2
2
  // Convex.state() pushes this directory when the store of the team answers an
3
- // older one or has no `version` yet. Raise it when a function changes in a way
4
- // that an older client cannot use. Never rename or remove a function: an
5
- // older client, and Samebase, call the store by these names.
6
- export const STORE_VERSION = 1;
3
+ // older one or has no `version` yet. Raise it when a function is added or
4
+ // changes in a way that an older client cannot use. Never rename or remove a
5
+ // function: an older client, and Samebase, call the store by these names.
6
+ //
7
+ // 1: `version`, `stacks`, `stages`, `list`, `get`, `replaced`, `getOutput`,
8
+ // `set`, `remove`, `removeStack`, `setOutput` (0.6.0).
9
+ // 2: `readStack` (0.7.0).
10
+ export const STORE_VERSION = 2;