@pepys/schema 0.0.0-stage → 0.1.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.
@@ -0,0 +1,64 @@
1
+ import type { ColumnKind, ConsumerInput, LinkTypeDeclaration, VersionHash } from '@pepys/kit';
2
+
3
+ // `consumers.json`, as `pepys build` writes it. Each entry is whole, so a kept version needs no file
4
+ // but its own entry.
5
+ export interface ConsumerBuildManifest {
6
+ readonly kit: 1;
7
+
8
+ // null for a plug-in built alone
9
+ readonly deployment: string | null;
10
+ readonly consumers: readonly ListedConsumer[];
11
+ }
12
+
13
+ export interface ConsumerVersion {
14
+ readonly name: string;
15
+
16
+ // the SHA-256 of the module's bytes
17
+ readonly module: VersionHash;
18
+
19
+ // enrichment champions; `{}` in v1
20
+ readonly pins: Readonly<Record<string, VersionHash>>;
21
+
22
+ // The SHA-256 of `module`, `pins` and `manifest` as JSON with sorted keys. The host names the
23
+ // version's folder by it and never computes it.
24
+ readonly version: VersionHash;
25
+ readonly manifest: ConsumerManifest;
26
+ }
27
+
28
+ export interface ListedConsumer extends ConsumerVersion {
29
+ // the module file, relative to `consumers.json`
30
+ readonly path: string;
31
+ }
32
+
33
+ export interface ConsumerManifest {
34
+ readonly name: string;
35
+ readonly input: ConsumerInput;
36
+
37
+ // the two symbols that the host calls
38
+ readonly exports: { readonly init: string; readonly apply: string };
39
+
40
+ // every read that the host serves the module, with its two symbols
41
+ readonly imports: readonly ManifestRead[];
42
+ readonly tables: Readonly<Record<string, ManifestTable>>;
43
+ readonly links: readonly LinkTypeDeclaration[];
44
+
45
+ // the consumer's tables, its indexes and its `entity` view, as the kit's SQL generator writes them
46
+ readonly schema: string;
47
+ }
48
+
49
+ // A `row` read serves a fact consumer one row of its own tables by key. A `scope_facts` read serves a
50
+ // scope consumer every fact of the scope's entity.
51
+ export type ManifestRead =
52
+ | { readonly kind: 'row'; readonly table: string; readonly length: string; readonly into: string }
53
+ | { readonly kind: 'scope_facts'; readonly length: string; readonly into: string };
54
+
55
+ export interface ManifestTable {
56
+ readonly columns: Readonly<Record<string, ManifestColumn>>;
57
+ readonly primaryKey: readonly string[];
58
+ readonly indexes: readonly (readonly string[])[];
59
+ }
60
+
61
+ export interface ManifestColumn {
62
+ readonly kind: ColumnKind;
63
+ readonly nullable: boolean;
64
+ }
@@ -0,0 +1,86 @@
1
+ // Generated by `bun run schema:generate`. Do not edit.
2
+
3
+ export interface GithubPullRequestRow {
4
+ id: string;
5
+ number: number;
6
+ repository_id: string;
7
+ title: string;
8
+ state: string;
9
+ is_draft: 0 | 1;
10
+ is_merged: 0 | 1;
11
+ author_id: string | null;
12
+ url: string;
13
+ merge_commit_sha: string | null;
14
+ created_at: string;
15
+ updated_at: string;
16
+ closed_at: string | null;
17
+ merged_at: string | null;
18
+ payload: unknown;
19
+ }
20
+
21
+ export interface GithubReleaseBumpRow {
22
+ pull_request_id: string;
23
+ release_repository: string;
24
+ release_tag: string;
25
+ }
26
+
27
+ export interface GithubReleaseRow {
28
+ id: string;
29
+ repository_id: string;
30
+ repository_full_name: string;
31
+ tag_name: string;
32
+ target_commitish: string;
33
+ name: string;
34
+ url: string;
35
+ is_draft: 0 | 1;
36
+ is_prerelease: 0 | 1;
37
+ created_at: string;
38
+ published_at: string | null;
39
+ updated_at: string;
40
+ deleted_at: string | null;
41
+ }
42
+
43
+ export interface GithubCommitRow {
44
+ repository_id: string;
45
+ sha: string;
46
+ committed_at: string;
47
+ }
48
+
49
+ export interface GithubDeployRow {
50
+ id: string;
51
+ repository_id: string;
52
+ environment: string;
53
+ target: string | null;
54
+ sha: string;
55
+ signal: string;
56
+ deployed_at: string;
57
+ }
58
+
59
+ export interface LedgerAccountRow {
60
+ id: string;
61
+ name: string | null;
62
+ balance: number;
63
+ is_closed: 0 | 1;
64
+ opened_at: string | null;
65
+ updated_at: string;
66
+ }
67
+
68
+ export interface LedgerEntryRow {
69
+ id: string;
70
+ account_id: string;
71
+ amount: number;
72
+ rate: number;
73
+ memo: string;
74
+ details: unknown;
75
+ posted_at: string;
76
+ }
77
+
78
+ export interface DB {
79
+ github_pull_request: GithubPullRequestRow;
80
+ github_release_bump: GithubReleaseBumpRow;
81
+ github_release: GithubReleaseRow;
82
+ github_commit: GithubCommitRow;
83
+ github_deploy: GithubDeployRow;
84
+ ledger_account: LedgerAccountRow;
85
+ ledger_entry: LedgerEntryRow;
86
+ }
@@ -0,0 +1,17 @@
1
+ import type { ConsumerDeclaration, TableDeclaration } from '@pepys/kit';
2
+
3
+ export interface TableEntry {
4
+ consumer: string;
5
+ name: string;
6
+ table: TableDeclaration;
7
+ }
8
+
9
+ export function collectTables(consumers: readonly ConsumerDeclaration[]): TableEntry[] {
10
+ return consumers.flatMap((consumer) =>
11
+ Object.entries(consumer.tables).map(([name, table]) => ({
12
+ consumer: consumer.name,
13
+ name,
14
+ table,
15
+ })),
16
+ );
17
+ }
@@ -0,0 +1,16 @@
1
+ import type { TableDeclaration } from '@pepys/kit';
2
+ import { toPascalCase } from '@pepys/kit/generators';
3
+ import { toTypeScriptType } from './to-typescript-type.ts';
4
+ import type { TypeScriptTypeNames } from './to-typescript-type.ts';
5
+
6
+ export function formatTypeScriptRow(
7
+ tableName: string,
8
+ table: TableDeclaration,
9
+ names: TypeScriptTypeNames,
10
+ ): string {
11
+ const fields = Object.entries(table.columns).map(
12
+ ([name, column]) => ` ${name}: ${toTypeScriptType(column, names)};`,
13
+ );
14
+
15
+ return [`export interface ${toPascalCase(tableName)}Row {`, ...fields, '}'].join('\n');
16
+ }
@@ -0,0 +1,21 @@
1
+ import type { ConsumerDeclaration } from '@pepys/kit';
2
+ import { toPascalCase } from '@pepys/kit/generators';
3
+ import { collectTables } from './collect-tables.ts';
4
+ import { formatTypeScriptRow } from './format-typescript-row.ts';
5
+
6
+ export function generateKyselyTypes(consumers: readonly ConsumerDeclaration[]): string {
7
+ const tables = collectTables(consumers);
8
+
9
+ // SQLite has no boolean type, so the driver reads a boolean column back as 0 or 1
10
+ const rows = tables.map((entry) =>
11
+ formatTypeScriptRow(entry.name, entry.table, { entityID: 'string', boolean: '0 | 1' }),
12
+ );
13
+
14
+ const database = [
15
+ 'export interface DB {',
16
+ ...tables.map((entry) => ` ${entry.name}: ${toPascalCase(entry.name)}Row;`),
17
+ '}',
18
+ ].join('\n');
19
+
20
+ return `${[...rows, database].join('\n\n')}\n`;
21
+ }
@@ -0,0 +1,19 @@
1
+ import type { ColumnDeclaration } from '@pepys/kit';
2
+ import { match } from 'ts-pattern';
3
+
4
+ export interface TypeScriptTypeNames {
5
+ entityID: string;
6
+ boolean: string;
7
+ }
8
+
9
+ export function toTypeScriptType(column: ColumnDeclaration, names: TypeScriptTypeNames): string {
10
+ const type = match(column.kind)
11
+ .with('text', 'timestamp', () => 'string')
12
+ .with('integer', 'real', () => 'number')
13
+ .with('boolean', () => names.boolean)
14
+ .with('json', () => 'unknown')
15
+ .with('entityID', () => names.entityID)
16
+ .exhaustive();
17
+
18
+ return column.nullable === true && column.kind !== 'json' ? `${type} | null` : type;
19
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ export { consumerBuildManifestSchema } from './consumer-manifest/consumer-build-manifest-schema.ts';
2
+ export type * from './consumer-execution/types.ts';
3
+ export type * from './consumer-manifest/types.ts';
4
+ export { generateKyselyTypes } from './generators/generate-kysely-types.ts';
5
+ export type { DB } from './generated/kysely-db.ts';
6
+ export { isEntityOfKind } from './link-registry/is-entity-of-kind.ts';
7
+ export type * from './link-registry/link-registry.ts';
8
+ export { makeShutdownHandler } from './service/make-shutdown-handler.ts';
9
+ export { parsePort } from './service/parse-port.ts';
@@ -0,0 +1,12 @@
1
+ import { parseEntityID } from '@pepys/kit';
2
+ import type { EntityID, EntityKind } from '@pepys/kit';
3
+ import type { KindedID } from './link-registry.ts';
4
+
5
+ export function isEntityOfKind<Kind extends EntityKind>(
6
+ id: EntityID,
7
+ kinds: readonly Kind[],
8
+ ): id is KindedID<Kind> {
9
+ const parsed = parseEntityID(id);
10
+
11
+ return parsed.ok && kinds.some((kind) => kind === `${parsed.value.source}:${parsed.value.kind}`);
12
+ }
@@ -0,0 +1,65 @@
1
+ import type {
2
+ ClientID,
3
+ EntityID,
4
+ EntityKind,
5
+ FactID,
6
+ LinkTypeDeclaration,
7
+ Reference,
8
+ } from '@pepys/kit';
9
+
10
+ // A plain string key, not a symbol: scriptc refuses the type-fest import that a symbol brand
11
+ // needs, and the brand a consumer generates must have the same shape.
12
+ export type KindedID<Kind extends EntityKind> = EntityID & { readonly '~entityKind': Kind };
13
+
14
+ export type LinkRegistry = Readonly<
15
+ Record<string, { readonly from: readonly EntityKind[]; readonly to: readonly EntityKind[] }>
16
+ >;
17
+
18
+ export type LinkRegistryOf<Declaration extends LinkTypeDeclaration> = {
19
+ readonly [Entry in Declaration as Entry['name']]: {
20
+ readonly from: Entry['from'];
21
+ readonly to: Entry['to'];
22
+ };
23
+ };
24
+
25
+ export type LinkTypeName<Registry extends LinkRegistry> = keyof Registry & string;
26
+
27
+ export type LinkTypeSources<
28
+ Registry extends LinkRegistry,
29
+ Name extends LinkTypeName<Registry>,
30
+ > = Registry[Name]['from'][number];
31
+
32
+ export type LinkTypeTargets<
33
+ Registry extends LinkRegistry,
34
+ Name extends LinkTypeName<Registry>,
35
+ > = Registry[Name]['to'][number];
36
+
37
+ export type LinkTarget<To extends EntityKind> =
38
+ | { kind: 'entity'; id: KindedID<To> }
39
+ | { kind: 'reference'; reference: Reference };
40
+
41
+ export type LinkOrigin =
42
+ | { kind: 'consumer'; name: string }
43
+ | { kind: 'manual'; client: ClientID; fact: FactID };
44
+
45
+ // A mapped union, not one object type: `Omit` over a union of link types loses the pairing of each
46
+ // type name with the kinds it allows.
47
+ export type LinkEnds<Registry extends LinkRegistry> = {
48
+ [Name in LinkTypeName<Registry>]: {
49
+ type: Name;
50
+ from: KindedID<LinkTypeSources<Registry, Name>>;
51
+ to: LinkTarget<LinkTypeTargets<Registry, Name>>;
52
+ };
53
+ }[LinkTypeName<Registry>];
54
+
55
+ export interface LinkDetails {
56
+ scope: string | null;
57
+ confidence: number;
58
+ evidence: string;
59
+ }
60
+
61
+ export type Link<Registry extends LinkRegistry> = LinkEnds<Registry> & LinkDetails;
62
+
63
+ export type EffectiveLink<Registry extends LinkRegistry> = Link<Registry> & { origin: LinkOrigin };
64
+
65
+ export type LinkRetraction<Registry extends LinkRegistry> = LinkEnds<Registry>;
@@ -0,0 +1,28 @@
1
+ interface ShutdownSteps {
2
+ stop: () => Promise<void>;
3
+ logError: (message: string, error: unknown) => void;
4
+ exit: (code: number) => void;
5
+ }
6
+
7
+ export function makeShutdownHandler(steps: ShutdownSteps): () => Promise<void> {
8
+ let shutdown: Promise<void> | undefined;
9
+
10
+ return () => {
11
+ shutdown ??= runShutdown(steps);
12
+
13
+ return shutdown;
14
+ };
15
+ }
16
+
17
+ async function runShutdown(steps: ShutdownSteps): Promise<void> {
18
+ try {
19
+ await steps.stop();
20
+ } catch (error: unknown) {
21
+ steps.logError('shutdown failed', error);
22
+ steps.exit(1);
23
+
24
+ return;
25
+ }
26
+
27
+ steps.exit(0);
28
+ }
@@ -0,0 +1,13 @@
1
+ import type { Result } from '@pepys/kit';
2
+
3
+ const digitsPattern = /^[0-9]+$/u;
4
+
5
+ export function parsePort(text: string): Result<number, { reason: 'not-a-port' }> {
6
+ const port = digitsPattern.test(text) ? Number(text) : Number.NaN;
7
+
8
+ if (port >= 1 && port <= 65_535) {
9
+ return { ok: true, value: port };
10
+ }
11
+
12
+ return { ok: false, failure: { reason: 'not-a-port' } };
13
+ }