@pepys/kit 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.
Files changed (68) 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 +445 -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 +112 -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 +80 -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 +37 -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/build-link-registry.ts +7 -0
  52. package/src/links/find-link-problem.ts +53 -0
  53. package/src/links/link-check-cases.json +274 -0
  54. package/src/links/types.ts +27 -0
  55. package/src/queries/define-query.ts +14 -0
  56. package/src/queries/types.ts +61 -0
  57. package/src/send/create-fact-batch-sender.ts +143 -0
  58. package/src/send/types.ts +25 -0
  59. package/src/send.ts +2 -0
  60. package/src/sources/define-fact-family.ts +10 -0
  61. package/src/sources/define-source.ts +7 -0
  62. package/src/sources/read-fact-batch.ts +145 -0
  63. package/src/sources/receive-fact-batch.ts +11 -0
  64. package/src/sources/types.ts +170 -0
  65. package/src/store-reader/store-reader.ts +76 -0
  66. package/src/tables/define-table.ts +13 -0
  67. package/src/tables/table-row.ts +20 -0
  68. package/src/types.ts +47 -0
@@ -0,0 +1,56 @@
1
+ import { kitVersion } from '../constants.ts';
2
+ import type { ConsumerDeclaration } from '../consumers/types.ts';
3
+ import type { EntityKind, LinkTypeDeclaration } from '../link-types/types.ts';
4
+ import type { AnyQueryDefinition } from '../queries/types.ts';
5
+ import type { SourceDefinition } from '../sources/types.ts';
6
+ import type { Plugin, PluginSettings } from './types.ts';
7
+
8
+ interface PluginDeclaration<Query extends AnyQueryDefinition> {
9
+ readonly name: string;
10
+ readonly sources?: readonly SourceDefinition[] | undefined;
11
+ readonly consumers?: readonly ConsumerDeclaration[] | undefined;
12
+ readonly queries?: readonly Query[] | undefined;
13
+ readonly settings?: PluginSettings | undefined;
14
+ }
15
+
16
+ export function definePlugin<const Query extends AnyQueryDefinition = never>(
17
+ plugin: PluginDeclaration<Query>,
18
+ ): Plugin<Query> {
19
+ const sources = plugin.sources ?? [];
20
+ const consumers = plugin.consumers ?? [];
21
+
22
+ return {
23
+ kit: kitVersion,
24
+ name: plugin.name,
25
+ settings: plugin.settings,
26
+ sources,
27
+ consumers,
28
+ queries: plugin.queries ?? [],
29
+ linkTypes: collectLinkTypes(consumers),
30
+ entityKinds: collectEntityKinds(sources, consumers),
31
+ };
32
+ }
33
+
34
+ function collectLinkTypes(consumers: readonly ConsumerDeclaration[]): LinkTypeDeclaration[] {
35
+ const byName = new Map<string, LinkTypeDeclaration>();
36
+
37
+ for (const linkType of consumers.flatMap((consumer) => consumer.links)) {
38
+ if (!byName.has(linkType.name)) {
39
+ byName.set(linkType.name, linkType);
40
+ }
41
+ }
42
+
43
+ return [...byName.values()];
44
+ }
45
+
46
+ function collectEntityKinds(
47
+ sources: readonly SourceDefinition[],
48
+ consumers: readonly ConsumerDeclaration[],
49
+ ): EntityKind[] {
50
+ const kinds = [
51
+ ...sources.flatMap((source) => source.families.map((family) => family.entityKind)),
52
+ ...consumers.flatMap((consumer) => consumer.entityViews.map((view) => view.kind)),
53
+ ];
54
+
55
+ return [...new Set(kinds)];
56
+ }
@@ -0,0 +1,27 @@
1
+ import type { Deployment, ReceiveRoute } from './types.ts';
2
+
3
+ export function listReceiveRoutes(deployment: Deployment): ReceiveRoute[] {
4
+ const routes: ReceiveRoute[] = [];
5
+
6
+ for (const source of deployment.plugins.flatMap((plugin) => plugin.sources)) {
7
+ const path = source.live;
8
+
9
+ if (path?.kind !== 'receive') {
10
+ continue;
11
+ }
12
+
13
+ const reach = path.reach ?? 'private';
14
+
15
+ for (const route of [path.route, ...(path.ignoredRoutes ?? [])]) {
16
+ routes.push({
17
+ source: source.name,
18
+ route,
19
+ check: path.check.kind,
20
+ reach,
21
+ stores: route === path.route,
22
+ });
23
+ }
24
+ }
25
+
26
+ return routes;
27
+ }
@@ -0,0 +1,112 @@
1
+ import type { z } from 'zod';
2
+ import type { ConsumerDeclaration } from '../consumers/types.ts';
3
+ import type { EntityKind, LinkTypeDeclaration } from '../link-types/types.ts';
4
+ import type { AnyQueryDefinition } from '../queries/types.ts';
5
+ import type { DeliveryCheck, SourceDefinition } from '../sources/types.ts';
6
+ import type { DeclaredStreamName } from '../types.ts';
7
+
8
+ export type SettingValue = string | number | boolean;
9
+
10
+ export interface Plugin<Query extends AnyQueryDefinition = AnyQueryDefinition> {
11
+ // the kit version the plug-in was declared with
12
+ readonly kit: number;
13
+ readonly name: string;
14
+ readonly settings: PluginSettings | undefined;
15
+ readonly sources: readonly SourceDefinition[];
16
+ readonly consumers: readonly ConsumerDeclaration[];
17
+ readonly queries: readonly Query[];
18
+
19
+ // every consumer's `links`, once per name
20
+ readonly linkTypes: readonly LinkTypeDeclaration[];
21
+
22
+ // every family's `entityKind` and every entity view's `kind`, once each
23
+ readonly entityKinds: readonly EntityKind[];
24
+ }
25
+
26
+ // Compile-time settings: `pepys build` writes the checked values into the glue of each consumer of
27
+ // the plug-in, so a consumer reads no setting at run time and a new value is a new version.
28
+ export interface PluginSettings<Schema extends z.ZodObject = z.ZodObject> {
29
+ // a flat object of strings, numbers and booleans
30
+ readonly schema: Schema;
31
+ readonly defaults: z.output<Schema>;
32
+ }
33
+
34
+ export interface Deployment<Plugins extends readonly Plugin[] = readonly Plugin[]> {
35
+ readonly name: string;
36
+ readonly plugins: Plugins;
37
+
38
+ // Literals by plug-in name: the deployment file reads no environment. A value left out takes the
39
+ // plug-in's default.
40
+ readonly settings?: Readonly<Record<string, Readonly<Record<string, SettingValue>>>> | undefined;
41
+ }
42
+
43
+ // The typed client of one plug-in's queries. A type only, so the kit needs no oRPC. A plug-in with
44
+ // no queries, written inside `defineDeployment`, takes its query type from the call's context, so
45
+ // a query name that is no literal gives no method rather than an index signature.
46
+ export type PluginClient<Target extends Plugin> = {
47
+ [Query in Target['queries'][number] as string extends Query['name'] ? never : Query['name']]: (
48
+ input: z.input<Query['input']>,
49
+ ) => Promise<z.output<Query['output']>>;
50
+ };
51
+
52
+ export interface CheckedDeployment {
53
+ readonly name: string;
54
+ readonly sources: readonly SourceDefinition[];
55
+ readonly consumers: readonly ConsumerDeclaration[];
56
+ readonly queries: readonly AnyQueryDefinition[];
57
+ readonly entityKinds: readonly EntityKind[];
58
+ readonly linkTypes: readonly LinkTypeDeclaration[];
59
+
60
+ // the checked settings of each plug-in that declares some, with the defaults filled in
61
+ readonly settings: Readonly<Record<string, Readonly<Record<string, SettingValue>>>>;
62
+ }
63
+
64
+ export type DuplicateOf =
65
+ | 'plugin'
66
+ | 'source'
67
+ | 'stream'
68
+ | 'route'
69
+ | 'fact-type'
70
+ | 'consumer'
71
+ | 'table'
72
+ | 'query'
73
+ | 'link-type'
74
+ | 'entity-view-kind';
75
+
76
+ export type DeploymentFailure =
77
+ | { kind: 'kit-version'; plugin: string; kit: number }
78
+
79
+ // `link-type`: one name with two shapes. `entity-view-kind`: a kind needs one owning table.
80
+ | { kind: 'duplicate'; of: DuplicateOf; name: string; plugins: readonly string[] }
81
+
82
+ // a link end or a scope kind that no plug-in of the deployment brings
83
+ | { kind: 'unknown-entity-kind'; entityKind: EntityKind; plugin: string; usedBy: string }
84
+ | { kind: 'missing-consumer'; consumer: string; query: string }
85
+ | { kind: 'missing-stream'; stream: DeclaredStreamName; consumer: string }
86
+ | { kind: 'reserved-query-name'; query: string }
87
+ | { kind: 'reserved-table-name'; table: string; consumer: string }
88
+ | { kind: 'scope-reads'; consumer: string }
89
+ | { kind: 'reserved-source-name'; source: string }
90
+
91
+ // a value that fails the schema, or settings for a plug-in that has none or is not deployed
92
+ | { kind: 'invalid-settings'; plugin: string; issues: readonly string[] }
93
+
94
+ // a read or an entity view that names a table the consumer does not declare
95
+ | { kind: 'unknown-table'; consumer: string; table: string; usedBy: 'reads' | 'entity-view' }
96
+ | {
97
+ kind: 'unknown-column';
98
+ consumer: string;
99
+ table: string;
100
+ column: string;
101
+ usedBy: 'primary-key' | 'index' | 'entity-view';
102
+ };
103
+
104
+ export interface ReceiveRoute {
105
+ source: string;
106
+ route: string;
107
+ check: DeliveryCheck['kind'];
108
+ reach: 'public' | 'private';
109
+
110
+ // false for a route that answers and stores nothing
111
+ stores: boolean;
112
+ }
@@ -0,0 +1,16 @@
1
+ import { toTagged } from '@zgeoff/std';
2
+ import type { EntityID } from '../ids/types.ts';
3
+ import type { Result } from '../types.ts';
4
+ import type { EntityIDFailure } from './entity-id-failure.ts';
5
+ import type { EntityIDParts } from './entity-id-parts.ts';
6
+ import { findEntityIDFailure } from './find-entity-id-failure.ts';
7
+
8
+ export function buildEntityID(parts: EntityIDParts): Result<EntityID, EntityIDFailure> {
9
+ const failure = findEntityIDFailure(parts);
10
+
11
+ if (failure !== undefined) {
12
+ return { ok: false, failure };
13
+ }
14
+
15
+ return { ok: true, value: toTagged<EntityID>(`${parts.source}:${parts.kind}:${parts.id}`) };
16
+ }
@@ -0,0 +1,6 @@
1
+ export type EntityIDFailure =
2
+ | { reason: 'malformed' }
3
+ | { reason: 'invalid-source' }
4
+ | { reason: 'reserved-source' }
5
+ | { reason: 'invalid-kind' }
6
+ | { reason: 'empty-id' };
@@ -0,0 +1,5 @@
1
+ export interface EntityIDParts {
2
+ source: string;
3
+ kind: string;
4
+ id: string;
5
+ }
@@ -0,0 +1,3 @@
1
+ // Each fact ID starts with its kind, so a source named like a kind could make a fact ID read as an
2
+ // entity ID.
3
+ export const factKinds = ['webhook', 'event', 'observation', 'enrichment', 'manual'] as const;
@@ -0,0 +1,25 @@
1
+ import type { EntityIDFailure } from './entity-id-failure.ts';
2
+ import type { EntityIDParts } from './entity-id-parts.ts';
3
+ import { factKinds } from './fact-kinds.ts';
4
+
5
+ const partPattern = /^[a-z][a-z0-9_]*$/u;
6
+
7
+ export function findEntityIDFailure(parts: EntityIDParts): EntityIDFailure | undefined {
8
+ if (!partPattern.test(parts.source)) {
9
+ return { reason: 'invalid-source' };
10
+ }
11
+
12
+ if (factKinds.some((factKind) => factKind === parts.source)) {
13
+ return { reason: 'reserved-source' };
14
+ }
15
+
16
+ if (!partPattern.test(parts.kind)) {
17
+ return { reason: 'invalid-kind' };
18
+ }
19
+
20
+ if (parts.id === '') {
21
+ return { reason: 'empty-id' };
22
+ }
23
+
24
+ return undefined;
25
+ }
@@ -0,0 +1,27 @@
1
+ import type { Result } from '../types.ts';
2
+ import type { EntityIDFailure } from './entity-id-failure.ts';
3
+ import type { EntityIDParts } from './entity-id-parts.ts';
4
+ import { findEntityIDFailure } from './find-entity-id-failure.ts';
5
+
6
+ export function parseEntityID(text: string): Result<EntityIDParts, EntityIDFailure> {
7
+ const firstSeparator = text.indexOf(':');
8
+ const secondSeparator = text.indexOf(':', firstSeparator + 1);
9
+
10
+ if (firstSeparator === -1 || secondSeparator === -1) {
11
+ return { ok: false, failure: { reason: 'malformed' } };
12
+ }
13
+
14
+ const parts = {
15
+ source: text.slice(0, firstSeparator),
16
+ kind: text.slice(firstSeparator + 1, secondSeparator),
17
+ id: text.slice(secondSeparator + 1),
18
+ };
19
+
20
+ const failure = findEntityIDFailure(parts);
21
+
22
+ if (failure !== undefined) {
23
+ return { ok: false, failure };
24
+ }
25
+
26
+ return { ok: true, value: parts };
27
+ }