ignotum 0.0.13 → 0.0.14

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 (39) hide show
  1. package/dist/cli/bin.mjs +369 -55
  2. package/dist/cli/bin.mjs.map +1 -1
  3. package/dist/runtime/client.d.ts +3 -3
  4. package/dist/runtime/client.js +24 -4
  5. package/dist/runtime/client.js.map +1 -1
  6. package/dist/runtime/descriptor-DCCz5AHd-_nhlnu8U.js +298 -0
  7. package/dist/runtime/descriptor-DCCz5AHd-_nhlnu8U.js.map +1 -0
  8. package/dist/runtime/id-E7JKqQ9r.d.ts +1 -0
  9. package/dist/runtime/index-DWU-7TIh.d.ts +185 -0
  10. package/dist/runtime/internal/host.d.ts +23 -5
  11. package/dist/runtime/internal/host.js +24 -4
  12. package/dist/runtime/internal/host.js.map +1 -1
  13. package/dist/runtime/internal/server.d.ts +1 -1
  14. package/dist/runtime/internal/server.js +1 -1
  15. package/dist/runtime/internal/types.d.ts +2 -1
  16. package/dist/runtime/internal/types.js +1 -1
  17. package/dist/runtime/{pagination-DnKg3dkI-r5ZUxBBx.d.ts → pagination-CpYCaRYD-C2Rpp4Cw.d.ts} +14 -4
  18. package/dist/runtime/pagination-E7JKqQ9r.d.ts +1 -0
  19. package/dist/runtime/{schema-1Zs03-iS.js → schema-DqogEydG.js} +38 -11
  20. package/dist/runtime/schema-DqogEydG.js.map +1 -0
  21. package/dist/runtime/server.d.ts +4 -3
  22. package/dist/runtime/server.js +1 -1
  23. package/dist/runtime/types-C1iDP2Yf-BnkfBOQf.d.ts +224 -0
  24. package/package.json +2 -2
  25. package/src/cli/agent-files.ts +4 -0
  26. package/src/cli/build/server.ts +35 -6
  27. package/src/cli/command.ts +25 -2
  28. package/src/cli/deploy.ts +34 -11
  29. package/src/client/sync.ts +1 -1
  30. package/src/dev-runtime/database.ts +77 -2
  31. package/src/dev-runtime/id.ts +19 -4
  32. package/src/dev-runtime/migrations.ts +9 -0
  33. package/src/internal/host.ts +1 -1
  34. package/dist/runtime/descriptor-Cyo9FP9n-C0SRVXNW.js +0 -154
  35. package/dist/runtime/descriptor-Cyo9FP9n-C0SRVXNW.js.map +0 -1
  36. package/dist/runtime/id-Dz0apuB3.d.ts +0 -1
  37. package/dist/runtime/index-D54flWtH.d.ts +0 -402
  38. package/dist/runtime/pagination-Dz0apuB3.d.ts +0 -1
  39. package/dist/runtime/schema-1Zs03-iS.js.map +0 -1
@@ -0,0 +1,224 @@
1
+ import { _ as fileFormats, g as FileValue, p as FileFormat, r as ErrorBrand, t as GeneratedId } from "./id-BzFHf3Wo-DGPCjgrf.js";
2
+ import { a as ValueDescriptor, i as DescriptorField, n as PaginationPage, o as ValueDescriptorCarrier, r as DescribedValue, s as ValueFields, t as PaginationOptions } from "./pagination-CpYCaRYD-C2Rpp4Cw.js";
3
+ import { Brand, Effect, Redacted, Schema, Struct } from "effect";
4
+ //#region ../contracts/dist/values-DpCxdMvK.d.ts
5
+ //#region src/schema/values.d.ts
6
+ declare const IdTargetsTypeId: unique symbol;
7
+ declare const UnresolvedReferencesTypeId: unique symbol;
8
+ interface IdTargets<TableName extends string> {
9
+ readonly [IdTargetsTypeId]: TableName;
10
+ }
11
+ interface UnresolvedReferences<TableName extends string> {
12
+ readonly [UnresolvedReferencesTypeId]: TableName;
13
+ }
14
+ type IdTargetsOf<Value> = Value extends IdTargets<infer TableName> ? TableName : never;
15
+ type UnresolvedReferencesOf<Value> = Value extends UnresolvedReferences<infer TableName> ? TableName : never;
16
+ type WithIdTargets<Value, TableName extends string> = [TableName] extends [never] ? Value : Value & IdTargets<TableName>;
17
+ type WithUnresolvedReferences<Value, TableName extends string> = [TableName] extends [never] ? Value : Value & UnresolvedReferences<TableName>;
18
+ type PreserveIdMetadata<Value, Source> = WithUnresolvedReferences<WithIdTargets<Value, IdTargetsOf<Source>>, UnresolvedReferencesOf<Source>>;
19
+ type PreserveValueMetadata<Value extends Schema.Constraint, Source> = Source extends ValueDescriptorCarrier ? DescribedValue<PreserveIdMetadata<Value, Source> & Schema.Constraint> : PreserveIdMetadata<Value, Source>;
20
+ type IdDefinition<TableName extends string> = DescribedValue<Schema.brand<typeof GeneratedId, `ignotum/id/${TableName}`> & IdTargets<TableName>>;
21
+ type ReferenceDefinition<TableName extends string> = IdDefinition<TableName> & UnresolvedReferences<TableName>;
22
+ type AddedFieldValidation<Fields, AddedFields> = Extract<keyof AddedFields, keyof Fields> extends never ? readonly [] : readonly [invalidFields: never];
23
+ type OverriddenFieldValidation<Fields, OverriddenFields> = Exclude<keyof OverriddenFields, keyof Fields> extends never ? readonly [] : readonly [invalidFields: never];
24
+ type PartialFields<Fields extends Schema.Struct.Fields> = { readonly [FieldName in keyof Fields]: Fields[FieldName] extends Schema.Constraint ? PreserveValueMetadata<Schema.optional<Fields[FieldName]>, Fields[FieldName]> : never; };
25
+ interface FixedObjectValidator<Fields extends Schema.Struct.Fields> extends Schema.Struct<Fields> {
26
+ readonly pick: <const Keys extends ReadonlyArray<keyof Fields>>(...keys: Keys) => ObjectDefinition<Pick<Fields, Keys[number]>>;
27
+ readonly omit: <const Keys extends ReadonlyArray<keyof Fields>>(...keys: Keys) => ObjectDefinition<Omit<Fields, Keys[number]>>;
28
+ readonly extend: <const AddedFields extends ValueFields>(fields: AddedFields, ...validation: AddedFieldValidation<Fields, AddedFields>) => ObjectDefinition<Struct.Assign<Fields, AddedFields>>;
29
+ readonly override: <const OverriddenFields extends Partial<Record<keyof Fields, DescribedValue>>>(fields: OverriddenFields, ...validation: OverriddenFieldValidation<Fields, OverriddenFields>) => ObjectDefinition<Struct.Assign<Fields, OverriddenFields>>;
30
+ readonly partial: () => ObjectDefinition<PartialFields<Fields>>;
31
+ }
32
+ type ObjectDefinition<Fields extends Schema.Struct.Fields> = DescribedValue<PreserveIdMetadata<FixedObjectValidator<Fields>, Fields[keyof Fields]>>;
33
+ type ResolvedFields<Fields extends Schema.Struct.Fields> = { readonly [FieldName in keyof Fields]: Omit<Fields[FieldName], typeof UnresolvedReferencesTypeId>; };
34
+ type LiteralValue = string | number | boolean;
35
+ interface IndexDefinition<Name extends string = string, FieldNames extends readonly string[] = readonly string[]> {
36
+ readonly name: Name;
37
+ readonly fields: FieldNames;
38
+ }
39
+ interface IndexDefinitions {
40
+ readonly [indexName: string]: IndexDefinition;
41
+ }
42
+ type IndexScalar = boolean | Date | number | string;
43
+ type IndexFieldName<Fields extends Schema.Struct.Fields> = { readonly [FieldName in keyof Fields & string]: Fields[FieldName]["Type"] extends IndexScalar ? FieldName : never; }[keyof Fields & string];
44
+ type IndexFieldTuple<Fields extends Schema.Struct.Fields> = readonly [IndexFieldName<Fields>, ...ReadonlyArray<IndexFieldName<Fields>>];
45
+ interface TableDefinition<Fields extends Schema.Struct.Fields, Indexes extends IndexDefinitions = {}> {
46
+ readonly _tag: "Table";
47
+ readonly descriptor: ValueDescriptor;
48
+ readonly fields: Fields;
49
+ readonly indexes: Indexes;
50
+ readonly schema: Schema.Struct<Fields>;
51
+ from<Previous extends DescribedValue>(previous: Previous, convert: (value: Previous["Type"]) => Schema.Struct.Type<Fields>): TableDefinition<Fields, Indexes>;
52
+ readonly index: <const Name extends string, const FieldNames extends IndexFieldTuple<Fields>>(name: Name extends keyof Indexes ? never : Name, fields: FieldNames) => TableDefinition<Fields, Indexes & Record<Name, IndexDefinition<Name, FieldNames>>>;
53
+ }
54
+ interface TableDefinitions {
55
+ readonly [tableName: string]: TableDefinition<Schema.Struct.Fields, IndexDefinitions>;
56
+ }
57
+ type TableNameOf$1<Definition extends TableDefinitions> = keyof Definition & string;
58
+ type ReservedFieldName = "createdAt" | "id" | "updatedAt";
59
+ type ReservedErrorTagName = "InternalServerError";
60
+ declare const ReservedFieldTypeId: unique symbol;
61
+ interface ReservedField<FieldName extends string> {
62
+ readonly [ReservedFieldTypeId]: `The field name '${FieldName}' is reserved by Ignotum`;
63
+ }
64
+ declare const ReservedErrorTagTypeId: unique symbol;
65
+ interface ReservedErrorTag<Tag extends string> {
66
+ readonly [ReservedErrorTagTypeId]: `The error tag '${Tag}' is reserved by Ignotum`;
67
+ }
68
+ type ErrorTagValidation<Tag extends string> = Tag extends ReservedErrorTagName ? ReservedErrorTag<Tag> : unknown;
69
+ type ReservedFieldValidation<Fields extends Schema.Struct.Fields> = { readonly [FieldName in Extract<keyof Fields, ReservedFieldName>]: ReservedField<FieldName>; };
70
+ type ReservedErrorFieldValidation<Fields extends Schema.Struct.Fields> = { readonly [FieldName in Extract<keyof Fields, "_tag">]: ReservedField<FieldName>; };
71
+ type ErrorDefinition<Tag extends string, Fields extends Schema.Struct.Fields> = DescribedValue<PreserveIdMetadata<Schema.brand<Schema.TaggedStruct<Tag, Fields>, typeof ErrorBrand> & ((fields: Schema.Struct.MakeIn<Fields>) => Schema.brand<Schema.TaggedStruct<Tag, Fields>, typeof ErrorBrand>["Type"]), Fields[keyof Fields]>>;
72
+ declare const table: <const Fields extends ValueFields>(fields: Fields & ReservedFieldValidation<NoInfer<Fields>>) => TableDefinition<Fields>;
73
+ declare const string: () => DescribedValue<Schema.String>;
74
+ declare const number: () => DescribedValue<Schema.Finite>;
75
+ declare const integer: () => DescribedValue<Schema.Int>;
76
+ declare const boolean: () => DescribedValue<Schema.Boolean>;
77
+ declare const date: () => DescribedValue<Schema.DateFromMillis>;
78
+ interface FileOptions<Formats extends readonly FileFormat[]> {
79
+ readonly formats?: Formats;
80
+ readonly maxBytes?: number;
81
+ }
82
+ declare const file: <const Formats extends readonly FileFormat[] = typeof fileFormats>(options?: FileOptions<Formats>) => DescribedValue<Schema.decodeTo<Schema.declare<FileValue<"avif" | "gif" | "jpeg" | "png" | "webp">, FileValue<"avif" | "gif" | "jpeg" | "png" | "webp">>, Schema.Struct<{
83
+ readonly $ignotum: Schema.Literal<"file">;
84
+ readonly id: Schema.brand<Schema.String, "ignotum/file/FileId"> & {
85
+ readonly idPrefix: "file";
86
+ };
87
+ readonly format: Schema.Literals<readonly ["jpeg", "png", "webp", "avif", "gif"]>;
88
+ readonly name: Schema.String;
89
+ readonly size: Schema.Int;
90
+ }>, never, never>> & Schema.Schema<FileValue<Formats[number]>>;
91
+ declare const never: () => DescribedValue<Schema.Never>;
92
+ declare const nullValue: () => DescribedValue<Schema.Null>;
93
+ declare const literal: <const Value extends LiteralValue>(value: Value) => DescribedValue<Schema.Literal<Value>>;
94
+ declare const literals: <const Members extends readonly [LiteralValue, LiteralValue, ...ReadonlyArray<LiteralValue>]>(...members: Members) => DescribedValue<Schema.Literals<Members>>;
95
+ declare const userId: () => DescribedValue<Schema.brand<Schema.String, "ignotum/id/UserId">>;
96
+ declare const id: <const TableName extends string>(tableName: TableName) => ReferenceDefinition<TableName>;
97
+ declare const array: <Value extends DescribedValue>(value: Value) => PreserveValueMetadata<Schema.$Array<Value>, Value>;
98
+ declare const object: <const Fields extends ValueFields>(fields: Fields) => ObjectDefinition<Fields>;
99
+ declare const optional: <Value extends DescribedValue>(value: Value) => PreserveValueMetadata<Schema.optional<Value>, Value>;
100
+ declare const nullable: <Value extends DescribedValue>(value: Value) => PreserveValueMetadata<Schema.NullOr<Value>, Value>;
101
+ declare const record: <Value extends DescribedValue>(value: Value) => PreserveValueMetadata<Schema.$Record<Schema.String, Value>, Value>;
102
+ declare const union: <const Members extends ReadonlyArray<DescribedValue>>(...members: Members) => PreserveValueMetadata<Schema.Union<Members>, Members[number]>;
103
+ type Secret<Value> = Redacted.Redacted<Value>;
104
+ declare const Secret: {
105
+ readonly isSecret: (u: unknown) => u is Redacted.Redacted<unknown>;
106
+ readonly value: <T>(self: Redacted.Redacted<T>) => T;
107
+ };
108
+ type SecretDefinition<Value extends DescribedValue> = DescribedValue<Schema.RedactedFromValue<Value>>;
109
+ declare function secret(): SecretDefinition<ReturnType<typeof string>>;
110
+ declare function secret<Value extends DescribedValue>(value: Value): SecretDefinition<Value>;
111
+ declare const pagination: () => ObjectDefinition<{
112
+ readonly cursor: DescribedValue<Schema.NullOr<DescribedValue<Schema.brand<Schema.String, "ignotum/runtime/PaginationCursor">>> & Schema.Constraint>;
113
+ readonly pageSize: DescribedValue<Schema.brand<Schema.Int, "ignotum/runtime/PaginationPageSize">>;
114
+ }>;
115
+ declare const page: <Value extends DescribedValue>(value: Value) => ObjectDefinition<{
116
+ readonly items: PreserveValueMetadata<Schema.$Array<Value>, Value>;
117
+ readonly nextCursor: DescribedValue<Schema.NullOr<DescribedValue<Schema.brand<Schema.String, "ignotum/runtime/PaginationCursor">>> & Schema.Constraint>;
118
+ }> & Schema.Schema<PaginationPage<Value["Type"]>>;
119
+ declare const error: <const Tag extends string, const Fields extends ValueFields>(tag: Tag & ErrorTagValidation<Tag>, fields: Fields & ReservedErrorFieldValidation<NoInfer<Fields>>) => ErrorDefinition<Tag, Fields>;
120
+ declare const values: {
121
+ userId: typeof userId;
122
+ array: typeof array;
123
+ boolean: typeof boolean;
124
+ date: typeof date;
125
+ file: typeof file;
126
+ error: typeof error;
127
+ id: typeof id;
128
+ integer: typeof integer;
129
+ literal: typeof literal;
130
+ literals: typeof literals;
131
+ never: typeof never;
132
+ null: typeof nullValue;
133
+ nullable: typeof nullable;
134
+ number: typeof number;
135
+ object: typeof object;
136
+ optional: typeof optional;
137
+ record: typeof record;
138
+ string: typeof string;
139
+ union: typeof union;
140
+ };
141
+ declare const environmentValues: {
142
+ array: typeof array;
143
+ boolean: typeof boolean;
144
+ date: typeof date;
145
+ integer: typeof integer;
146
+ literal: typeof literal;
147
+ literals: typeof literals;
148
+ never: typeof never;
149
+ null: typeof nullValue;
150
+ nullable: typeof nullable;
151
+ number: typeof number;
152
+ object: typeof object;
153
+ optional: typeof optional;
154
+ record: typeof record;
155
+ secret: typeof secret;
156
+ string: typeof string;
157
+ union: typeof union;
158
+ };
159
+ type EnvironmentValues = typeof environmentValues;
160
+ type DocumentDefinition<Definition extends TableDefinitions, TableName extends TableNameOf$1<Definition>> = ObjectDefinition<Struct.Assign<ResolvedFields<Definition[TableName]["fields"]>, {
161
+ readonly id: IdDefinition<TableName>;
162
+ readonly createdAt: typeof Schema.DateFromMillis;
163
+ readonly updatedAt: typeof Schema.DateFromMillis;
164
+ }>>;
165
+ type BoundValues<Definition extends TableDefinitions> = Omit<typeof values, "id"> & {
166
+ readonly doc: <const TableName extends TableNameOf$1<Definition>>(tableName: TableName) => DocumentDefinition<Definition, TableName>;
167
+ readonly id: <const TableName extends TableNameOf$1<Definition>>(tableName: TableName) => IdDefinition<TableName>;
168
+ readonly page: typeof page;
169
+ readonly pagination: () => ReturnType<typeof pagination> & Schema.Schema<PaginationOptions>;
170
+ };
171
+ //#endregion
172
+ //#region ../contracts/dist/types-C1iDP2Yf.d.ts
173
+ //#region src/schema/environment.d.ts
174
+ declare const EnvironmentDefinitionTypeId: unique symbol;
175
+ declare const EnvironmentDecoderTypeId: unique symbol;
176
+ type EnvironmentFields = {
177
+ readonly [fieldName: PropertyKey]: DescribedValue<Schema.ConstraintDecoder<unknown, never>>;
178
+ };
179
+ interface DefinedEnv<Fields extends EnvironmentFields = EnvironmentFields> {
180
+ readonly _tag: "Environment";
181
+ readonly fields: Fields;
182
+ readonly descriptors: ReadonlyArray<DescriptorField>;
183
+ readonly schema: Schema.Struct<Fields>;
184
+ readonly [EnvironmentDefinitionTypeId]: Fields;
185
+ readonly [EnvironmentDecoderTypeId]: (raw: Readonly<Record<string, string>>) => Effect.Effect<Readonly<EnvironmentType<DefinedEnv<Fields>>>, EnvironmentValueInvalid>;
186
+ }
187
+ interface EnvironmentAuthoring {
188
+ readonly values: EnvironmentValues;
189
+ }
190
+ type EnvironmentType<Environment extends DefinedEnv> = Schema.Struct.Type<Environment[typeof EnvironmentDefinitionTypeId]>;
191
+ declare const EnvironmentValueInvalid_base: Schema.Class<EnvironmentValueInvalid, Schema.TaggedStruct<"EnvironmentValueInvalid", {
192
+ readonly key: Schema.optional<Schema.String>;
193
+ readonly message: Schema.String;
194
+ }>, import("effect/Cause").YieldableError>;
195
+ declare class EnvironmentValueInvalid extends EnvironmentValueInvalid_base {}
196
+ declare const defineEnv: <const Fields extends EnvironmentFields>(define: (authoring: EnvironmentAuthoring) => Fields) => DefinedEnv<Fields>;
197
+ declare const emptyEnv: DefinedEnv<{}>;
198
+ //#endregion
199
+ //#region src/schema/types.d.ts
200
+ /** A document identifier branded with the table it belongs to. */
201
+ type Id<TableName extends string> = GeneratedId & Brand.Brand<`ignotum/id/${TableName}`>;
202
+ declare const SchemaDefinitionTypeId: unique symbol;
203
+ interface SchemaDefinitionContract<Definition> {
204
+ readonly [SchemaDefinitionTypeId]: Definition;
205
+ }
206
+ type SchemaDefinitionOf<SchemaDefinition> = SchemaDefinition extends SchemaDefinitionContract<infer Definition> ? Definition : never;
207
+ interface TableDefinitionLike {
208
+ readonly fields: Schema.Struct.Fields;
209
+ }
210
+ interface TablesLike {
211
+ readonly [tableName: string]: TableDefinitionLike;
212
+ }
213
+ type TableNameOf<Definition extends TablesLike> = keyof Definition & string;
214
+ interface SystemFields<TableName extends string> {
215
+ readonly id: Id<TableName>;
216
+ readonly createdAt: Date;
217
+ readonly updatedAt: Date;
218
+ }
219
+ type DocumentFields<TableName extends string, Table extends TableDefinitionLike> = SystemFields<TableName> & Schema.Struct.Type<Table["fields"]>;
220
+ /** The complete document returned for a table, including Ignotum system fields. */
221
+ type Document<Definition extends TablesLike, TableName extends TableNameOf<Definition>> = { readonly [FieldName in keyof DocumentFields<TableName, Definition[TableName]>]: DocumentFields<TableName, Definition[TableName]>[FieldName]; };
222
+ //#endregion
223
+ export { table as _, Id as a, SchemaDefinitionTypeId as c, BoundValues as d, IdTargetsOf as f, UnresolvedReferencesTypeId as g, TableDefinition as h, EnvironmentType as i, defineEnv as l, Secret as m, Document as n, SchemaDefinitionContract as o, IndexDefinitions as p, EnvironmentAuthoring as r, SchemaDefinitionOf as s, DefinedEnv as t, emptyEnv as u, values as v };
224
+ //# sourceMappingURL=types-C1iDP2Yf-BnkfBOQf.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ignotum",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "description": "Ignotum is an opinionated TypeScript application cloud.",
5
5
  "author": "Johannes Schießl",
6
6
  "repository": {
@@ -81,8 +81,8 @@
81
81
  "@effect/sql-sqlite-node": "4.0.0-rc.111",
82
82
  "redis": "6.2.1",
83
83
  "@ignotum/contracts": "1.0.0",
84
- "@ignotum/deployment": "1.0.0",
85
84
  "@ignotum/runtime": "1.0.0",
85
+ "@ignotum/deployment": "1.0.0",
86
86
  "@ignotum/shared": "1.0.0"
87
87
  },
88
88
  "engines": {
@@ -8,6 +8,7 @@ import appEntry from "../../../../docs/user/app-entry.md?raw";
8
8
  import cli from "../../../../docs/user/cli.md?raw";
9
9
  import database from "../../../../docs/user/database.md?raw";
10
10
  import deployApp from "../../../../docs/user/deploy-app.md?raw";
11
+ import consoleGuide from "../../../../docs/user/console.md?raw";
11
12
  import deploymentModel from "../../../../docs/user/deployment-model.md?raw";
12
13
  import deployments from "../../../../docs/user/deployments.md?raw";
13
14
  import errors from "../../../../docs/user/errors.md?raw";
@@ -21,6 +22,7 @@ import index from "../../../../docs/user/index.md?raw";
21
22
  import indexes from "../../../../docs/user/indexes.md?raw";
22
23
  import limits from "../../../../docs/user/limits.md?raw";
23
24
  import localDevelopment from "../../../../docs/user/local-development.md?raw";
25
+ import migrations from "../../../../docs/user/migrations.md?raw";
24
26
  import mutations from "../../../../docs/user/mutations.md?raw";
25
27
  import pagination from "../../../../docs/user/pagination.md?raw";
26
28
  import preact from "../../../../docs/user/preact.md?raw";
@@ -49,6 +51,7 @@ const references = [
49
51
  ["cli.md", cli],
50
52
  ["database.md", database],
51
53
  ["deploy-app.md", deployApp],
54
+ ["console.md", consoleGuide],
52
55
  ["deployment-model.md", deploymentModel],
53
56
  ["deployments.md", deployments],
54
57
  ["errors.md", errors],
@@ -62,6 +65,7 @@ const references = [
62
65
  ["indexes.md", indexes],
63
66
  ["limits.md", limits],
64
67
  ["local-development.md", localDevelopment],
68
+ ["migrations.md", migrations],
65
69
  ["mutations.md", mutations],
66
70
  ["pagination.md", pagination],
67
71
  ["preact.md", preact],
@@ -513,16 +513,28 @@ const discoverServerFunctions = Effect.fn("Deploy.discoverServerFunctionsIsolate
513
513
  );
514
514
  });
515
515
 
516
- const serverFunctionEntryPlugin = (definition: DiscoveredServerFunction): Plugin => {
516
+ const serverFunctionEntryPlugin = (
517
+ definition: DiscoveredServerFunction,
518
+ schemaPath?: string,
519
+ ): Plugin => {
517
520
  const entryId = `virtual:ignotum/server-function/${definition.address}`;
518
521
  const resolvedEntryId = `\0${entryId}`;
519
- const source = `import { call } from ${encodeJavaScriptString(hostModuleId)}
522
+ const source =
523
+ schemaPath === undefined
524
+ ? `import { call } from ${encodeJavaScriptString(hostModuleId)}
520
525
  import { invoke } from "ignotum/internal/host"
521
526
  import { ${definition.exportName} as definition } from ${encodeJavaScriptString(normalizePath(definition.modulePath))}
522
527
 
523
528
  export default async function handler(encodedRequest, encodedEnvironment) {
524
529
  return invoke(definition, JSON.parse(encodedRequest), call, JSON.parse(encodedEnvironment))
525
530
  }
531
+ `
532
+ : `import { evolve } from "ignotum/internal/host"
533
+ import schema from ${encodeJavaScriptString(normalizePath(schemaPath))}
534
+
535
+ export default function handler(encodedRequest) {
536
+ return evolve(schema, encodedRequest)
537
+ }
526
538
  `;
527
539
 
528
540
  return {
@@ -537,6 +549,7 @@ const buildServerFunction = Effect.fn("Deploy.buildServerFunction")(function* (
537
549
  serverOutputDirectory: string,
538
550
  conditions: ReadonlyArray<string>,
539
551
  definition: DiscoveredServerFunction,
552
+ schemaPath?: string,
540
553
  ) {
541
554
  const fileSystem = yield* FileSystem.FileSystem;
542
555
  const path = yield* Path.Path;
@@ -573,7 +586,7 @@ const buildServerFunction = Effect.fn("Deploy.buildServerFunction")(function* (
573
586
  configFile: false,
574
587
  logLevel: "warn",
575
588
  mode: "production",
576
- plugins: [boundaries, serverFunctionEntryPlugin(definition)],
589
+ plugins: [boundaries, serverFunctionEntryPlugin(definition, schemaPath)],
577
590
  publicDir: false,
578
591
  resolve: {
579
592
  conditions: [...conditions],
@@ -599,8 +612,9 @@ const buildServerFunction = Effect.fn("Deploy.buildServerFunction")(function* (
599
612
  }
600
613
  if (
601
614
  entry.dynamicImports.length > 0 ||
602
- entry.imports.length !== 1 ||
603
- entry.imports[0] !== hostModuleId ||
615
+ (schemaPath === undefined
616
+ ? entry.imports.length !== 1 || entry.imports[0] !== hostModuleId
617
+ : entry.imports.length !== 0) ||
604
618
  !entry.exports.includes("default")
605
619
  ) {
606
620
  return yield* InvalidBuildOutput.make({
@@ -667,8 +681,23 @@ export const buildServer = Effect.fn("Deploy.buildServer")(function* (
667
681
  (definition) => buildServerFunction(appDirectory, outputDirectory, conditions, definition),
668
682
  { concurrency: 4 },
669
683
  );
684
+ const schemaPath = path.join(appDirectory, "server", "schema.ts");
685
+ const schemaEvolution = yield* buildServerFunction(
686
+ appDirectory,
687
+ outputDirectory,
688
+ conditions,
689
+ {
690
+ address: FunctionAddress.make("api._schema.evolve"),
691
+ exportName: "evolve",
692
+ kind: "Mutation",
693
+ moduleName: "_schema",
694
+ modulePath: schemaPath,
695
+ },
696
+ schemaPath,
697
+ );
670
698
  const manifest = {
671
- formatVersion: 3,
699
+ formatVersion: 4,
700
+ schemaEvolution: { bundle: schemaEvolution.bundle, sourceMap: schemaEvolution.sourceMap },
672
701
  environment: {
673
702
  artifact: yield* artifactReference(serverEnvironmentPath, environmentBytes),
674
703
  fields: discovered.environment.fields,
@@ -13,7 +13,8 @@ import {
13
13
  status as readAuthStatus,
14
14
  } from "./auth-client.js";
15
15
  import { ControlClient } from "./control-client.js";
16
- import { deploy as runDeploy } from "./deploy.js";
16
+ import { deploy as runDeploy, resumeDeployment } from "./deploy.js";
17
+ import { DeploymentId } from "@ignotum/contracts/runtime/identity";
17
18
  import { resetDevDatabase } from "../dev-runtime/dev-database.js";
18
19
  import { dev as runDev } from "./dev.js";
19
20
  import { createApp } from "./new-app.js";
@@ -144,15 +145,37 @@ const dev = Command.make(
144
145
  const deploy = Command.make(
145
146
  "deploy",
146
147
  {
148
+ resume: Flag.string("resume").pipe(
149
+ Flag.withDescription("Finish activation of an already uploaded deployment."),
150
+ Flag.withSchema(DeploymentId),
151
+ Flag.optional,
152
+ ),
147
153
  app: Flag.string("app").pipe(
148
154
  Flag.withDescription("Create or link this app slug on the first deploy."),
149
155
  Flag.withSchema(AppSlug),
150
156
  Flag.optional,
151
157
  ),
152
158
  },
153
- Effect.fn("deploy")(function* ({ app }) {
159
+ Effect.fn("deploy")(function* ({ app, resume }) {
154
160
  const path = yield* Path.Path;
155
161
  const terminal = yield* Terminal.Terminal;
162
+ if (Option.isSome(resume)) {
163
+ const result = yield* resumeDeployment(
164
+ path.resolve("."),
165
+ resume.value,
166
+ Option.getOrUndefined(app),
167
+ ).pipe(
168
+ // @effect-diagnostics-next-line strictEffectProvide:off This command owns the authenticated control client.
169
+ Effect.provide(ControlClient.layer),
170
+ Effect.mapError((error) =>
171
+ CliError.UserError.make({ cause: error, userMessage: error.message }),
172
+ ),
173
+ );
174
+ yield* terminal.display(
175
+ `Deployment activated.\n\nApp: ${result.appUrl}\nDeployment: ${result.deploymentId}\n`,
176
+ );
177
+ return;
178
+ }
156
179
  const result = yield* runDeploy(path.resolve("."), Option.getOrUndefined(app)).pipe(
157
180
  // @effect-diagnostics-next-line strictEffectProvide:off The deploy subcommand owns this client layer for its full lifetime.
158
181
  Effect.provide(ControlClient.layer),
package/src/cli/deploy.ts CHANGED
@@ -15,7 +15,12 @@ import { appUrl, configureApp, type AppConfiguration } from "./app-configuration
15
15
  import { buildClient } from "./build/client.js";
16
16
  import { buildServer } from "./build/server.js";
17
17
  import { generate } from "./codegen.js";
18
- import { ControlClient, deploymentUploadFiles } from "./control-client.js";
18
+ import {
19
+ ControlClient,
20
+ deploymentUploadFiles,
21
+ type ControlApiError,
22
+ type ControlClientUnavailable,
23
+ } from "./control-client.js";
19
24
 
20
25
  export interface DeploymentBuildResult {
21
26
  readonly clientAssets: number;
@@ -58,6 +63,18 @@ export class DeploymentActivationUnconfirmed extends Schema.TaggedError<Deployme
58
63
  },
59
64
  ) {}
60
65
 
66
+ const activationError = (
67
+ deploymentId: DeploymentId,
68
+ cause: ControlApiError | ControlClientUnavailable,
69
+ ) =>
70
+ cause._tag === "ControlClientUnavailable" || cause.status >= 500
71
+ ? DeploymentActivationUnconfirmed.make({
72
+ cause,
73
+ deploymentId,
74
+ message: `Deployment ${deploymentId} is ready, but activation could not be confirmed. Run ignotum deploy --resume ${deploymentId} to finish activation.`,
75
+ })
76
+ : cause;
77
+
61
78
  const replaceBuildDirectory = Effect.fn("Deploy.replaceBuildDirectory")(function* (
62
79
  stateDirectory: string,
63
80
  stagingDirectory: string,
@@ -192,16 +209,9 @@ export const uploadDeployment = Effect.fn("Deploy.upload")(function* (
192
209
  }),
193
210
  );
194
211
 
195
- yield* control.activateDeployment(configuration.appId, deployment.id).pipe(
196
- Effect.catchTags({
197
- ControlClientUnavailable: (cause) =>
198
- DeploymentActivationUnconfirmed.make({
199
- cause,
200
- deploymentId: deployment.id,
201
- message: `Deployment ${deployment.id} is ready, but activation could not be confirmed.`,
202
- }),
203
- }),
204
- );
212
+ yield* control
213
+ .activateDeployment(configuration.appId, deployment.id)
214
+ .pipe(Effect.mapError((cause) => activationError(deployment.id, cause)));
205
215
 
206
216
  return deployment.id;
207
217
  });
@@ -220,3 +230,16 @@ export const deploy = Effect.fn("Deploy.run")(function* (
220
230
  deploymentId,
221
231
  } satisfies DeployResult;
222
232
  });
233
+
234
+ export const resumeDeployment = Effect.fn("Deploy.resume")(function* (
235
+ appDirectory: string,
236
+ deploymentId: DeploymentId,
237
+ requestedSlug?: AppSlug,
238
+ ) {
239
+ const configuration = yield* configureApp(appDirectory, requestedSlug);
240
+ const control = yield* ControlClient;
241
+ yield* control
242
+ .activateDeployment(configuration.appId, deploymentId)
243
+ .pipe(Effect.mapError((cause) => activationError(deploymentId, cause)));
244
+ return { appUrl: appUrl(configuration.slug), deploymentId };
245
+ });
@@ -67,7 +67,7 @@ interface QueryEntry {
67
67
  result: QueryResult<unknown, ErrorValue> | ClientInfrastructureError;
68
68
  }
69
69
 
70
- interface QueryObserver {
70
+ export interface QueryObserver {
71
71
  readonly getSnapshot: () => QueryResult<unknown, ErrorValue> | ClientInfrastructureError;
72
72
  readonly release: Effect.Effect<void>;
73
73
  readonly subscribe: (listener: Listener) => () => void;
@@ -43,10 +43,15 @@ import type {
43
43
  RuntimeTableDefinition,
44
44
  } from "@ignotum/contracts/runtime/schema";
45
45
  import {
46
+ applyValueDefaults,
47
+ descriptorFields,
48
+ evolveStoredFields,
46
49
  getValueDescriptor,
47
50
  SchemaDefinitionTypeId,
48
51
  type ValueDescriptor,
49
52
  } from "@ignotum/contracts/schema";
53
+ import { SchemaSnapshot } from "@ignotum/contracts/deployment";
54
+ import { encodeCanonical } from "@ignotum/deployment";
50
55
  import type { FileValue } from "@ignotum/contracts/schema/file";
51
56
 
52
57
  import { DevelopmentDatabase } from "./migrations.js";
@@ -272,9 +277,13 @@ const encodeFields = Effect.fn("LocalDatabase.encodeFields")(function* (
272
277
  operation: Extract<DatabaseOperation, "insert" | "patch" | "replace">,
273
278
  id: string | null,
274
279
  ) {
275
- return yield* Schema.encodeEffect(fieldsCodec(table), {
280
+ const normalized = yield* Effect.try({
281
+ try: () => (operation === "patch" ? value : applyValueDefaults(table.schema, value)),
282
+ catch: (cause) => schemaMismatch(tableName, id, operation, cause),
283
+ });
284
+ return yield* Schema.encodeUnknownEffect(fieldsCodec(table), {
276
285
  onExcessProperty: "error",
277
- })(value).pipe(Effect.mapError((cause) => schemaMismatch(tableName, id, operation, cause)));
286
+ })(normalized).pipe(Effect.mapError((cause) => schemaMismatch(tableName, id, operation, cause)));
278
287
  });
279
288
 
280
289
  const decodeFields = Effect.fn("LocalDatabase.decodeFields")(function* (
@@ -492,6 +501,70 @@ export class LocalDatabase extends Context.Service<LocalDatabase, LocalDatabaseS
492
501
  }
493
502
  });
494
503
 
504
+ const prepareSchema = Effect.fn("LocalDatabase.prepareSchema")(function* (
505
+ schema: RuntimeSchemaDefinition,
506
+ ) {
507
+ const definitions = schema[SchemaDefinitionTypeId];
508
+ const snapshot = encodeCanonical(SchemaSnapshot, {
509
+ formatVersion: 2,
510
+ tables: Object.entries(definitions)
511
+ .sort(([left], [right]) => left.localeCompare(right))
512
+ .map(([name, definition]) => ({
513
+ name,
514
+ fields: descriptorFields(definition.fields),
515
+ indexes: Object.values(definition.indexes).sort((left, right) =>
516
+ left.name.localeCompare(right.name),
517
+ ),
518
+ })),
519
+ });
520
+ const current = yield* sql<{
521
+ snapshot: string;
522
+ }>`SELECT snapshot FROM schema_state WHERE singleton = 1`;
523
+ if (current[0]?.snapshot === snapshot) return;
524
+ let changedData = false;
525
+ const storedTables = yield* sql<{ id: string; name: string }>`SELECT id, name FROM tables`;
526
+ for (const stored of storedTables) {
527
+ if (Object.hasOwn(definitions, stored.name)) continue;
528
+ const documents =
529
+ yield* sql`SELECT 1 FROM documents WHERE tableId = ${stored.id} LIMIT 1`;
530
+ if (documents.length > 0)
531
+ return yield* Effect.die(`Cannot remove non-empty table '${stored.name}'.`);
532
+ yield* sql`DELETE FROM tables WHERE id = ${stored.id}`;
533
+ }
534
+ for (const [name, definition] of Object.entries(definitions)) {
535
+ const table = yield* resolveTable(schema, name);
536
+ let cursor = "";
537
+ let changed = false;
538
+ while (true) {
539
+ const rows = yield* sql<{ id: string; fields: string }>`
540
+ SELECT id, fields FROM documents WHERE tableId = ${table.id} AND id > ${cursor}
541
+ ORDER BY id LIMIT 100
542
+ `;
543
+ if (rows.length === 0) break;
544
+ for (const row of rows) {
545
+ const fields = yield* Effect.try({
546
+ try: () =>
547
+ JSON.stringify(evolveStoredFields(definition.schema, JSON.parse(row.fields))),
548
+ catch: () =>
549
+ new Error(
550
+ `Document '${row.id}' in '${name}' does not match the new schema or its conversion rules.`,
551
+ ),
552
+ }).pipe(Effect.orDie);
553
+ if (fields !== row.fields) {
554
+ yield* sql`UPDATE documents SET fields = ${fields} WHERE tableId = ${table.id} AND id = ${row.id}`;
555
+ changed = true;
556
+ changedData = true;
557
+ }
558
+ cursor = row.id;
559
+ }
560
+ }
561
+ if (changed) yield* sql`DELETE FROM indexes WHERE tableId = ${table.id}`;
562
+ }
563
+ yield* sql`INSERT INTO schema_state (singleton, snapshot) VALUES (1, ${snapshot})
564
+ ON CONFLICT (singleton) DO UPDATE SET snapshot = excluded.snapshot`;
565
+ if (current.length > 0 || changedData) yield* advanceRevision(undefined);
566
+ });
567
+
495
568
  const indexKeys = Effect.fn("LocalDatabase.indexKeys")(function* (
496
569
  table: ResolvedTable,
497
570
  row: StoredDocument,
@@ -1026,6 +1099,7 @@ export class LocalDatabase extends Context.Service<LocalDatabase, LocalDatabaseS
1026
1099
  ) =>
1027
1100
  sql.withTransaction(
1028
1101
  Effect.gen(function* () {
1102
+ yield* prepareSchema(schema).pipe(Effect.orDie);
1029
1103
  yield* prepareIndexes(schema).pipe(Effect.orDie);
1030
1104
  const dependencies = makeDependencyRecorder<ReadDependency>();
1031
1105
  const context = Object.freeze({
@@ -1044,6 +1118,7 @@ export class LocalDatabase extends Context.Service<LocalDatabase, LocalDatabaseS
1044
1118
  ) =>
1045
1119
  sql.withTransaction(
1046
1120
  Effect.gen(function* () {
1121
+ yield* prepareSchema(schema).pipe(Effect.orDie);
1047
1122
  yield* prepareIndexes(schema).pipe(Effect.orDie);
1048
1123
  const invalidations = makeDependencyRecorder<WriteInvalidation>();
1049
1124
  const context = Object.freeze({
@@ -1,4 +1,14 @@
1
- import { Context, Effect, FileSystem, Layer, Path, Schema, Semaphore } from "effect";
1
+ import {
2
+ Context,
3
+ Effect,
4
+ FileSystem,
5
+ Layer,
6
+ Path,
7
+ Predicate,
8
+ Record,
9
+ Schema,
10
+ Semaphore,
11
+ } from "effect";
2
12
  import {
3
13
  DevelopmentUsername,
4
14
  developmentSelectionKey,
@@ -31,9 +41,14 @@ export const LocalCompletion = Schema.Struct({
31
41
 
32
42
  const projection = (user: User, share: ReadonlyArray<string>): User => ({
33
43
  id: user.id,
34
- name: share.includes("name") ? user.name : undefined,
35
- email: share.includes("email") ? user.email : undefined,
36
- image: share.includes("image") ? user.image : undefined,
44
+ ...Record.filter(
45
+ {
46
+ name: share.includes("name") ? user.name : undefined,
47
+ email: share.includes("email") ? user.email : undefined,
48
+ image: share.includes("image") ? user.image : undefined,
49
+ },
50
+ Predicate.isNotUndefined,
51
+ ),
37
52
  });
38
53
 
39
54
  export class LocalId extends Context.Service<
@@ -88,10 +88,19 @@ const addApplicationFiles = Effect.gen(function* () {
88
88
  `;
89
89
  });
90
90
 
91
+ const addSchemaState = Effect.gen(function* () {
92
+ const sql = yield* SqlClient.SqlClient;
93
+ yield* sql`CREATE TABLE schema_state (
94
+ singleton INTEGER PRIMARY KEY NOT NULL CHECK (singleton = 1),
95
+ snapshot TEXT NOT NULL CHECK (json_valid(snapshot))
96
+ ) STRICT`;
97
+ });
98
+
91
99
  export const developmentMigrationLoader = Migrator.fromRecord({
92
100
  "0001_initial_development_schema": initialDevelopmentSchema,
93
101
  "0002_application_indexes": addDevelopmentIndexes,
94
102
  "0003_application_files": addApplicationFiles,
103
+ "0004_schema_state": addSchemaState,
95
104
  });
96
105
 
97
106
  interface DevelopmentDatabaseService {