@pepys/kit 0.0.0-stage → 0.0.1

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 (67) hide show
  1. package/README.md +63 -2
  2. package/package.json +34 -4
  3. package/src/constants.ts +1 -0
  4. package/src/consumers/define-consumer.ts +20 -0
  5. package/src/consumers/types.ts +48 -0
  6. package/src/delivery-checks/build-delivery-proof.ts +12 -0
  7. package/src/delivery-checks/is-delivery-authentic.ts +49 -0
  8. package/src/deployments/check-deployment.ts +444 -0
  9. package/src/deployments/define-deployment.ts +7 -0
  10. package/src/deployments/define-plugin.ts +56 -0
  11. package/src/deployments/list-receive-routes.ts +27 -0
  12. package/src/deployments/types.ts +110 -0
  13. package/src/entity-ids/build-entity-id.ts +16 -0
  14. package/src/entity-ids/entity-id-failure.ts +6 -0
  15. package/src/entity-ids/entity-id-parts.ts +5 -0
  16. package/src/entity-ids/fact-kinds.ts +3 -0
  17. package/src/entity-ids/find-entity-id-failure.ts +25 -0
  18. package/src/entity-ids/parse-entity-id.ts +27 -0
  19. package/src/fact-envelope-cases.json +464 -0
  20. package/src/facts/build-enrichment-fact-id.ts +15 -0
  21. package/src/facts/build-event-fact-id.ts +11 -0
  22. package/src/facts/build-fact-hash.ts +6 -0
  23. package/src/facts/build-manual-fact-id.ts +7 -0
  24. package/src/facts/build-observation-fact-id.ts +15 -0
  25. package/src/facts/build-webhook-fact-id.ts +11 -0
  26. package/src/facts/parse-fact-envelope.ts +44 -0
  27. package/src/facts/parse-stream-name.ts +33 -0
  28. package/src/generators/build-payload-shape.ts +75 -0
  29. package/src/generators/collect-view-kinds.ts +8 -0
  30. package/src/generators/format-column-type.ts +16 -0
  31. package/src/generators/format-entity-ids.ts +46 -0
  32. package/src/generators/format-fact-reader.ts +194 -0
  33. package/src/generators/format-literal.ts +13 -0
  34. package/src/generators/format-model-contract.ts +23 -0
  35. package/src/generators/format-operations.ts +109 -0
  36. package/src/generators/format-payload-shape.ts +181 -0
  37. package/src/generators/format-property-access.ts +6 -0
  38. package/src/generators/format-property-name.ts +6 -0
  39. package/src/generators/format-row-readers.ts +116 -0
  40. package/src/generators/generate-boundary-glue.ts +60 -0
  41. package/src/generators/generate-sqlite-schema.ts +78 -0
  42. package/src/generators/is-identifier.ts +5 -0
  43. package/src/generators/quote-sql-identifier.ts +3 -0
  44. package/src/generators/to-entity-id-type-name.ts +6 -0
  45. package/src/generators/to-pascal-case.ts +7 -0
  46. package/src/generators.ts +4 -0
  47. package/src/ids/types.ts +25 -0
  48. package/src/index.ts +36 -0
  49. package/src/link-types/define-link-type.ts +9 -0
  50. package/src/link-types/types.ts +15 -0
  51. package/src/links/find-link-problem.ts +53 -0
  52. package/src/links/link-check-cases.json +274 -0
  53. package/src/links/types.ts +22 -0
  54. package/src/queries/define-query.ts +14 -0
  55. package/src/queries/types.ts +56 -0
  56. package/src/send/create-fact-batch-sender.ts +143 -0
  57. package/src/send/types.ts +25 -0
  58. package/src/send.ts +2 -0
  59. package/src/sources/define-fact-family.ts +10 -0
  60. package/src/sources/define-source.ts +7 -0
  61. package/src/sources/read-fact-batch.ts +114 -0
  62. package/src/sources/receive-fact-batch.ts +11 -0
  63. package/src/sources/types.ts +154 -0
  64. package/src/store-reader/store-reader.ts +76 -0
  65. package/src/tables/define-table.ts +13 -0
  66. package/src/tables/table-row.ts +20 -0
  67. package/src/types.ts +47 -0
@@ -0,0 +1,116 @@
1
+ import { match } from 'ts-pattern';
2
+ import type { ConsumerDeclaration } from '../consumers/types.ts';
3
+ import type { EntityKind } from '../link-types/types.ts';
4
+ import type { ColumnDeclaration } from '../types.ts';
5
+ import { collectViewKinds } from './collect-view-kinds.ts';
6
+ import { formatLiteral } from './format-literal.ts';
7
+ import { formatPropertyAccess } from './format-property-access.ts';
8
+ import { formatPropertyName } from './format-property-name.ts';
9
+ import { toEntityIDTypeName } from './to-entity-id-type-name.ts';
10
+ import { toPascalCase } from './to-pascal-case.ts';
11
+
12
+ // One reader per table in `reads`, through the two host functions of the table: the length of the
13
+ // row's JSON, or -1 for no row, and a copy of it into a buffer.
14
+ export function formatRowReaders(consumer: ConsumerDeclaration): string[] {
15
+ if (consumer.reads.length === 0) {
16
+ return [];
17
+ }
18
+
19
+ const viewKinds = collectViewKinds(consumer);
20
+
21
+ return [
22
+ ...consumer.reads.flatMap((tableName) => {
23
+ const table = consumer.tables[tableName];
24
+
25
+ return table === undefined
26
+ ? []
27
+ : [formatRowReader(tableName, Object.entries(table.columns), viewKinds)];
28
+ }),
29
+ [
30
+ 'function isEntityID(value: unknown): value is EntityID {',
31
+ " return typeof value === 'string';",
32
+ '}',
33
+ ].join('\n'),
34
+ ];
35
+ }
36
+
37
+ function formatRowReader(
38
+ tableName: string,
39
+ columns: readonly (readonly [string, ColumnDeclaration])[],
40
+ viewKinds: ReadonlyMap<string, EntityKind>,
41
+ ): string {
42
+ const name = toPascalCase(tableName);
43
+
44
+ const checks = columns.flatMap(([columnName, column]) =>
45
+ formatCheck(columnName, column, viewKinds.get(`${tableName}.${columnName}`)),
46
+ );
47
+
48
+ const fields = columns.map(
49
+ ([columnName]) =>
50
+ ` ${formatPropertyName(columnName)}: ${formatPropertyAccess('parsed', columnName)},`,
51
+ );
52
+
53
+ return [
54
+ `declare function read${name}Length(key: string): number;`,
55
+ `declare function read${name}Into(key: string, row: Uint8Array): void;`,
56
+ '',
57
+ `export function read${name}Row(key: string): ${name}Row | undefined {`,
58
+ ` const length = read${name}Length(key);`,
59
+ '',
60
+ ' if (length < 0) {',
61
+ ' return undefined;',
62
+ ' }',
63
+ '',
64
+ ' const row = new Uint8Array(length);',
65
+ '',
66
+ ` read${name}Into(key, row);`,
67
+ '',
68
+ ' const parsed: unknown = JSON.parse(new TextDecoder().decode(row));',
69
+ '',
70
+ " if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {",
71
+ ' return undefined;',
72
+ ' }',
73
+ '',
74
+ ...checks,
75
+ ' return {',
76
+ ...fields,
77
+ ' };',
78
+ '}',
79
+ ].join('\n');
80
+ }
81
+
82
+ function formatCheck(
83
+ columnName: string,
84
+ column: ColumnDeclaration,
85
+ kind: EntityKind | undefined,
86
+ ): string[] {
87
+ const value = formatPropertyAccess('parsed', columnName);
88
+ const mismatch = formatMismatch(value, column, kind);
89
+ const conditions = [`!(${formatLiteral(columnName)} in parsed)`, ...mismatch];
90
+
91
+ return [` if (${conditions.join(' || ')}) {`, ' return undefined;', ' }', ''];
92
+ }
93
+
94
+ function formatMismatch(
95
+ value: string,
96
+ column: ColumnDeclaration,
97
+ kind: EntityKind | undefined,
98
+ ): string[] {
99
+ const mismatch = match(column.kind)
100
+ .with('text', 'timestamp', () => `typeof ${value} !== 'string'`)
101
+ .with('integer', 'real', () => `typeof ${value} !== 'number'`)
102
+ .with('boolean', () => `typeof ${value} !== 'boolean'`)
103
+ .with('entityID', () =>
104
+ kind === undefined
105
+ ? `!isEntityID(${value})`
106
+ : `typeof ${value} !== 'string' || !is${toEntityIDTypeName(kind)}(${value})`,
107
+ )
108
+ .with('json', () => null)
109
+ .exhaustive();
110
+
111
+ if (mismatch === null) {
112
+ return [];
113
+ }
114
+
115
+ return [column.nullable === true ? `(${value} !== null && (${mismatch}))` : `(${mismatch})`];
116
+ }
@@ -0,0 +1,60 @@
1
+ import type { ConsumerDeclaration } from '../consumers/types.ts';
2
+ import type { EntityKind } from '../link-types/types.ts';
3
+ import type { FactFamily } from '../sources/types.ts';
4
+ import { formatEntityIDs } from './format-entity-ids.ts';
5
+ import { formatFactReader } from './format-fact-reader.ts';
6
+ import { formatModelContract } from './format-model-contract.ts';
7
+ import { formatOperations } from './format-operations.ts';
8
+ import { formatRowReaders } from './format-row-readers.ts';
9
+ import { toPascalCase } from './to-pascal-case.ts';
10
+
11
+ export interface BoundaryGlueParameters {
12
+ consumer: ConsumerDeclaration;
13
+
14
+ // The families of the deployment. The glue types the facts of those on the consumer's streams.
15
+ families: readonly FactFamily[];
16
+
17
+ // the plug-in's checked settings, or undefined when it declares none
18
+ settings: Readonly<Record<string, string | number | boolean>> | undefined;
19
+ }
20
+
21
+ // The one file that a consumer's entry imports: entity IDs, the settings, the model contract, the
22
+ // facts it reads, its rows and its operations. The glue reads a row of the consumer's own tables
23
+ // only, and runs no zod.
24
+ export function generateBoundaryGlue(parameters: BoundaryGlueParameters): string {
25
+ const consumer = parameters.consumer;
26
+ const consumerName = toPascalCase(consumer.name);
27
+
28
+ const streams = new Set<string>(consumer.input.streams);
29
+
30
+ const families = parameters.families.filter((family) => streams.has(family.stream));
31
+
32
+ const sections = [
33
+ ...formatEntityIDs(collectEntityKinds(consumer, families)),
34
+ ...(parameters.settings === undefined ? [] : [formatSettings(parameters.settings)]),
35
+ formatModelContract(consumerName),
36
+ ...formatFactReader({ consumerName, input: consumer.input, families }),
37
+ ...formatOperations(consumer),
38
+ ...formatRowReaders(consumer),
39
+ ];
40
+
41
+ return `${sections.join('\n\n')}\n`;
42
+ }
43
+
44
+ function collectEntityKinds(
45
+ consumer: ConsumerDeclaration,
46
+ families: readonly FactFamily[],
47
+ ): EntityKind[] {
48
+ const kinds = new Set<EntityKind>([
49
+ ...(consumer.input.mode === 'scope' ? [consumer.input.scopeKind] : []),
50
+ ...families.map((family) => family.entityKind),
51
+ ...consumer.entityViews.map((view) => view.kind),
52
+ ...consumer.links.flatMap((link) => [...link.from, ...link.to]),
53
+ ]);
54
+
55
+ return [...kinds].toSorted();
56
+ }
57
+
58
+ function formatSettings(settings: Readonly<Record<string, string | number | boolean>>): string {
59
+ return `export const settings = ${JSON.stringify(settings, null, 2)} as const;`;
60
+ }
@@ -0,0 +1,78 @@
1
+ import { match } from 'ts-pattern';
2
+ import type { ConsumerDeclaration, EntityViewDeclaration } from '../consumers/types.ts';
3
+ import type { ColumnDeclaration } from '../types.ts';
4
+ import { quoteSqlIdentifier } from './quote-sql-identifier.ts';
5
+
6
+ export function generateSqliteSchema(consumer: ConsumerDeclaration): string {
7
+ const statements: string[] = [];
8
+
9
+ for (const [name, table] of Object.entries(consumer.tables)) {
10
+ statements.push(formatCreateTable(name, table.columns, table.primaryKey));
11
+
12
+ for (const index of table.indexes) {
13
+ const indexName = quoteSqlIdentifier(`${name}_${index.columns.join('_')}_idx`);
14
+ const columns = index.columns.map((column) => quoteSqlIdentifier(column)).join(', ');
15
+
16
+ statements.push(`CREATE INDEX ${indexName} ON ${quoteSqlIdentifier(name)} (${columns});`);
17
+ }
18
+ }
19
+
20
+ if (consumer.entityViews.length > 0) {
21
+ statements.push(formatEntityView(consumer.entityViews));
22
+ }
23
+
24
+ return `${statements.join('\n\n')}\n`;
25
+ }
26
+
27
+ function formatCreateTable(
28
+ name: string,
29
+ columns: Readonly<Record<string, ColumnDeclaration>>,
30
+ primaryKey: readonly string[],
31
+ ): string {
32
+ const definitions = Object.entries(columns).map(
33
+ ([columnName, column]) =>
34
+ ` ${quoteSqlIdentifier(columnName)} ${toSqliteType(column)}${column.nullable === true ? '' : ' NOT NULL'}`,
35
+ );
36
+
37
+ return [
38
+ `CREATE TABLE ${quoteSqlIdentifier(name)} (`,
39
+ [
40
+ ...definitions,
41
+ ` PRIMARY KEY (${primaryKey.map((column) => quoteSqlIdentifier(column)).join(', ')})`,
42
+ ].join(',\n'),
43
+ ');',
44
+ ].join('\n');
45
+ }
46
+
47
+ function toSqliteType(column: ColumnDeclaration): string {
48
+ return match(column.kind)
49
+ .with('text', 'json', 'entityID', 'timestamp', () => 'TEXT')
50
+ .with('integer', 'boolean', () => 'INTEGER')
51
+ .with('real', () => 'REAL')
52
+ .exhaustive();
53
+ }
54
+
55
+ function formatEntityView(views: readonly EntityViewDeclaration[]): string {
56
+ const selects = views.map((view) => formatEntitySelect(view)).join('\nUNION ALL\n');
57
+
58
+ return `CREATE VIEW "entity" AS\n${selects};`;
59
+ }
60
+
61
+ function formatEntitySelect(view: EntityViewDeclaration): string {
62
+ const kind = `'${view.kind.replaceAll("'", "''")}'`;
63
+
64
+ const selections = [
65
+ `${quoteSqlIdentifier(view.id)} AS "id"`,
66
+ `${kind} AS "kind"`,
67
+ `${quoteSqlIdentifier(view.title)} AS "title"`,
68
+ `${formatOptionalColumn(view.url)} AS "url"`,
69
+ `${formatOptionalColumn(view.actor)} AS "actor"`,
70
+ `${quoteSqlIdentifier(view.updatedAt)} AS "updated_at"`,
71
+ ].map((selection) => ` ${selection}`);
72
+
73
+ return ['SELECT', selections.join(',\n'), `FROM ${quoteSqlIdentifier(view.table)}`].join('\n');
74
+ }
75
+
76
+ function formatOptionalColumn(column: string | null): string {
77
+ return column === null ? 'NULL' : quoteSqlIdentifier(column);
78
+ }
@@ -0,0 +1,5 @@
1
+ const identifierPattern = /^[A-Za-z_$][\w$]*$/u;
2
+
3
+ export function isIdentifier(name: string): boolean {
4
+ return identifierPattern.test(name);
5
+ }
@@ -0,0 +1,3 @@
1
+ export function quoteSqlIdentifier(name: string): string {
2
+ return `"${name.replaceAll('"', '""')}"`;
3
+ }
@@ -0,0 +1,6 @@
1
+ import type { EntityKind } from '../link-types/types.ts';
2
+ import { toPascalCase } from './to-pascal-case.ts';
3
+
4
+ export function toEntityIDTypeName(kind: EntityKind): string {
5
+ return `${toPascalCase(kind)}ID`;
6
+ }
@@ -0,0 +1,7 @@
1
+ export function toPascalCase(name: string): string {
2
+ return name
3
+ .split(/[^A-Za-z0-9]+/u)
4
+ .filter((word) => word !== '')
5
+ .map((word) => word.slice(0, 1).toUpperCase() + word.slice(1))
6
+ .join('');
7
+ }
@@ -0,0 +1,4 @@
1
+ export { generateBoundaryGlue } from './generators/generate-boundary-glue.ts';
2
+ export type { BoundaryGlueParameters } from './generators/generate-boundary-glue.ts';
3
+ export { generateSqliteSchema } from './generators/generate-sqlite-schema.ts';
4
+ export { toPascalCase } from './generators/to-pascal-case.ts';
@@ -0,0 +1,25 @@
1
+ import type { Tagged } from 'type-fest';
2
+
3
+ export type EntityID = Tagged<string, 'EntityID'>;
4
+
5
+ export type FactID = Tagged<string, 'FactID'>;
6
+
7
+ export type StreamName = Tagged<string, 'StreamName'>;
8
+
9
+ export type StreamOffset = Tagged<number, 'StreamOffset'>;
10
+
11
+ export type ScopeID = Tagged<string, 'ScopeID'>;
12
+
13
+ export type BlobHash = Tagged<string, 'BlobHash'>;
14
+
15
+ export type VersionHash = Tagged<string, 'VersionHash'>;
16
+
17
+ export type Reference = Tagged<string, 'Reference'>;
18
+
19
+ export type BucketNumber = Tagged<number, 'BucketNumber'>;
20
+
21
+ export type ClientID = Tagged<string, 'ClientID'>;
22
+
23
+ export type GrantID = Tagged<string, 'GrantID'>;
24
+
25
+ export type RequestID = Tagged<string, 'RequestID'>;
package/src/index.ts ADDED
@@ -0,0 +1,36 @@
1
+ export { buildEnrichmentFactID } from './facts/build-enrichment-fact-id.ts';
2
+ export { buildEntityID } from './entity-ids/build-entity-id.ts';
3
+ export { buildEventFactID } from './facts/build-event-fact-id.ts';
4
+ export { buildManualFactID } from './facts/build-manual-fact-id.ts';
5
+ export { buildObservationFactID } from './facts/build-observation-fact-id.ts';
6
+ export { buildWebhookFactID } from './facts/build-webhook-fact-id.ts';
7
+ export { checkDeployment } from './deployments/check-deployment.ts';
8
+ export { defineConsumer } from './consumers/define-consumer.ts';
9
+ export { defineDeployment } from './deployments/define-deployment.ts';
10
+ export { defineFactFamily } from './sources/define-fact-family.ts';
11
+ export { defineLinkType } from './link-types/define-link-type.ts';
12
+ export { definePlugin } from './deployments/define-plugin.ts';
13
+ export { defineQuery } from './queries/define-query.ts';
14
+ export { defineSource } from './sources/define-source.ts';
15
+ export { defineTable } from './tables/define-table.ts';
16
+ export { findLinkProblem } from './links/find-link-problem.ts';
17
+ export { isDeliveryAuthentic } from './delivery-checks/is-delivery-authentic.ts';
18
+ export type { EntityIDFailure } from './entity-ids/entity-id-failure.ts';
19
+ export type { EntityIDParts } from './entity-ids/entity-id-parts.ts';
20
+ export { listReceiveRoutes } from './deployments/list-receive-routes.ts';
21
+ export { parseEntityID } from './entity-ids/parse-entity-id.ts';
22
+ export { parseFactEnvelope } from './facts/parse-fact-envelope.ts';
23
+ export { parseStreamName } from './facts/parse-stream-name.ts';
24
+ export { readFactBatch } from './sources/read-fact-batch.ts';
25
+ export { receiveFactBatch } from './sources/receive-fact-batch.ts';
26
+ export type { TableRow } from './tables/table-row.ts';
27
+ export type * from './consumers/types.ts';
28
+ export type * from './deployments/types.ts';
29
+ export type * from './ids/types.ts';
30
+ export type * from './link-types/types.ts';
31
+ export type * from './links/types.ts';
32
+ export type * from './send/types.ts';
33
+ export type * from './queries/types.ts';
34
+ export type * from './sources/types.ts';
35
+ export type * from './store-reader/store-reader.ts';
36
+ export type * from './types.ts';
@@ -0,0 +1,9 @@
1
+ import type { EntityKind, LinkTypeDeclaration } from './types.ts';
2
+
3
+ export function defineLinkType<
4
+ const Name extends string,
5
+ const From extends EntityKind,
6
+ const To extends EntityKind,
7
+ >(linkType: LinkTypeDeclaration<Name, From, To>): LinkTypeDeclaration<Name, From, To> {
8
+ return linkType;
9
+ }
@@ -0,0 +1,15 @@
1
+ // `<source>:<kind>`, in an entity view, a link end, a scope kind and an entity ID alike.
2
+ export type EntityKind = `${string}:${string}`;
3
+
4
+ export interface LinkTypeDeclaration<
5
+ Name extends string = string,
6
+ From extends EntityKind = EntityKind,
7
+ To extends EntityKind = EntityKind,
8
+ > {
9
+ readonly name: Name;
10
+ readonly from: readonly From[];
11
+ readonly to: readonly To[];
12
+
13
+ // `neighbourhood` follows a hub link type only when the caller asks
14
+ readonly hub: boolean;
15
+ }
@@ -0,0 +1,53 @@
1
+ import { parseEntityID } from '../entity-ids/parse-entity-id.ts';
2
+ import type { CheckedLink, LinkCheckConsumer, LinkProblem } from './types.ts';
3
+
4
+ // The link check, from the consumer's declaration alone: it looks no row up, so a link to an
5
+ // entity that has not arrived passes. It reports the first problem in this order, which the host's
6
+ // Rust check keeps: the type, the `from` end, the `to` end, the confidence, the evidence.
7
+ export function findLinkProblem(
8
+ consumer: LinkCheckConsumer,
9
+ link: CheckedLink,
10
+ ): LinkProblem | undefined {
11
+ const linkType = consumer.links.find((declared) => declared.name === link.type);
12
+
13
+ if (linkType === undefined) {
14
+ return { kind: 'not-declared', type: link.type, consumer: consumer.name };
15
+ }
16
+
17
+ const endProblem =
18
+ findEndProblem('from', link.from, linkType.from) ?? findEndProblem('to', link.to, linkType.to);
19
+
20
+ if (endProblem !== undefined) {
21
+ return endProblem;
22
+ }
23
+
24
+ if (!(link.confidence >= 0 && link.confidence <= 1)) {
25
+ return { kind: 'bad-confidence', confidence: link.confidence };
26
+ }
27
+
28
+ if (link.evidence === '') {
29
+ return { kind: 'no-evidence' };
30
+ }
31
+
32
+ return undefined;
33
+ }
34
+
35
+ function findEndProblem(
36
+ end: 'from' | 'to',
37
+ id: string,
38
+ allowed: readonly string[],
39
+ ): LinkProblem | undefined {
40
+ const parsed = parseEntityID(id);
41
+
42
+ if (!parsed.ok) {
43
+ return { kind: 'not-an-entity-id', end, id };
44
+ }
45
+
46
+ const kind = `${parsed.value.source}:${parsed.value.kind}`;
47
+
48
+ if (!allowed.some((allowedKind) => allowedKind === kind)) {
49
+ return { kind: 'wrong-kind', end, id, allowed };
50
+ }
51
+
52
+ return undefined;
53
+ }