@malloy-publisher/server 0.0.223 → 0.0.225

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 (43) hide show
  1. package/dist/app/api-doc.yaml +75 -0
  2. package/dist/app/assets/{EnvironmentPage-3wCdljov.js → EnvironmentPage-DYTeXDll.js} +1 -1
  3. package/dist/app/assets/{HomePage-K0GHwqq2.js → HomePage-pDK2BPJY.js} +1 -1
  4. package/dist/app/assets/{LightMode-CwU3kN4I.js → LightMode-C2bwGPY1.js} +1 -1
  5. package/dist/app/assets/{MainPage-CTncHE5T.js → MainPage-WtBulMH_.js} +2 -2
  6. package/dist/app/assets/{MaterializationsPage-DziAOD6-.js → MaterializationsPage-hMgOtflG.js} +1 -1
  7. package/dist/app/assets/{ModelPage-XrS2jXEc.js → ModelPage-B2N5kYII.js} +1 -1
  8. package/dist/app/assets/{PackagePage-BkwgFxG8.js → PackagePage-CEN90nQG.js} +1 -1
  9. package/dist/app/assets/{RouteError-9cIQga6p.js → RouteError-BG2c5Zf0.js} +1 -1
  10. package/dist/app/assets/{ThemeEditorPage-DWiCRfEe.js → ThemeEditorPage-DNfeUwEZ.js} +1 -1
  11. package/dist/app/assets/{WorkbookPage-Ce7FM_Po.js → WorkbookPage-NKI1BhFS.js} +1 -1
  12. package/dist/app/assets/{core-D9Hl0IDY.es-CrO01m4X.js → core-C6anj5c0.es-DDLHqpzt.js} +1 -1
  13. package/dist/app/assets/{index-D2sm5RBA.js → index-C6gZ6sSY.js} +5 -5
  14. package/dist/app/assets/{index-CtQm7kvp.js → index-CzNqKMVl.js} +1 -1
  15. package/dist/app/assets/{index-wJU_kzl2.js → index-DMQtnaf4.js} +2 -2
  16. package/dist/app/assets/{index-Dz8hgkmy.js → index-JXgvyZna.js} +1 -1
  17. package/dist/app/assets/{index-8E2uLeV9.js → index-OEjKNSYb.js} +2 -2
  18. package/dist/app/index.html +1 -1
  19. package/dist/server.mjs +411 -26
  20. package/dist/sshcrypto-8m50vnmb.node +0 -0
  21. package/package.json +12 -12
  22. package/src/controller/materialization.controller.spec.ts +72 -0
  23. package/src/controller/materialization.controller.ts +75 -11
  24. package/src/mcp/skills/skills_bundle.json +1 -1
  25. package/src/service/build_plan.spec.ts +119 -0
  26. package/src/service/build_plan.ts +143 -0
  27. package/src/service/environment.ts +3 -3
  28. package/src/service/freshness.spec.ts +183 -0
  29. package/src/service/freshness.ts +112 -0
  30. package/src/service/manifest_loader.spec.ts +33 -0
  31. package/src/service/manifest_loader.ts +17 -8
  32. package/src/service/materialization_cron_gate.spec.ts +61 -17
  33. package/src/service/materialization_service.spec.ts +42 -0
  34. package/src/service/materialization_service.ts +46 -3
  35. package/src/service/materialization_test_fixtures.ts +47 -14
  36. package/src/service/model.ts +54 -4
  37. package/src/service/package.ts +173 -35
  38. package/src/storage/DatabaseInterface.ts +24 -0
  39. package/tests/fixtures/persist-multi-level/data/orders.csv +5 -0
  40. package/tests/fixtures/persist-multi-level/multi_level.malloy +18 -0
  41. package/tests/fixtures/persist-multi-level/publisher.json +5 -0
  42. package/tests/integration/materialization/freshness_gate.integration.spec.ts +292 -0
  43. package/tests/integration/materialization/reference_manifest.integration.spec.ts +251 -0
Binary file
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@malloy-publisher/server",
3
3
  "description": "Malloy Publisher Server",
4
- "version": "0.0.223",
4
+ "version": "0.0.225",
5
5
  "main": "dist/server.mjs",
6
6
  "bin": {
7
7
  "malloy-publisher": "dist/server.mjs"
@@ -36,17 +36,17 @@
36
36
  "@azure/storage-blob": "^12.26.0",
37
37
  "@duckdb/node-api": "1.5.3-r.2",
38
38
  "@google-cloud/storage": "^7.16.0",
39
- "@malloydata/db-bigquery": "^0.0.421",
40
- "@malloydata/db-databricks": "^0.0.421",
41
- "@malloydata/db-duckdb": "^0.0.421",
42
- "@malloydata/db-mysql": "^0.0.421",
43
- "@malloydata/db-postgres": "^0.0.421",
44
- "@malloydata/db-publisher": "^0.0.421",
45
- "@malloydata/db-snowflake": "^0.0.421",
46
- "@malloydata/db-trino": "^0.0.421",
47
- "@malloydata/malloy": "^0.0.421",
48
- "@malloydata/malloy-sql": "^0.0.421",
49
- "@malloydata/render-validator": "^0.0.421",
39
+ "@malloydata/db-bigquery": "^0.0.422",
40
+ "@malloydata/db-databricks": "^0.0.422",
41
+ "@malloydata/db-duckdb": "^0.0.422",
42
+ "@malloydata/db-mysql": "^0.0.422",
43
+ "@malloydata/db-postgres": "^0.0.422",
44
+ "@malloydata/db-publisher": "^0.0.422",
45
+ "@malloydata/db-snowflake": "^0.0.422",
46
+ "@malloydata/db-trino": "^0.0.422",
47
+ "@malloydata/malloy": "^0.0.422",
48
+ "@malloydata/malloy-sql": "^0.0.422",
49
+ "@malloydata/render-validator": "^0.0.422",
50
50
  "@modelcontextprotocol/sdk": "^1.13.2",
51
51
  "@opentelemetry/api": "^1.9.0",
52
52
  "@opentelemetry/auto-instrumentations-node": "^0.57.0",
@@ -85,6 +85,78 @@ describe("MaterializationController.createMaterialization validation", () => {
85
85
  expect(await parse({ buildInstructions: null })).toEqual({});
86
86
  });
87
87
 
88
+ it("parses referenceManifest and strictUpstreams alongside sources", async () => {
89
+ const parsed = await parse({
90
+ buildInstructions: {
91
+ sources: [
92
+ {
93
+ sourceEntityId: "b2",
94
+ materializedTableId: "mt-2",
95
+ physicalTableName: "downstream_v1",
96
+ realization: "COPY",
97
+ },
98
+ ],
99
+ referenceManifest: [
100
+ { sourceEntityId: "b1", physicalTableName: "upstream_table" },
101
+ ],
102
+ strictUpstreams: true,
103
+ },
104
+ });
105
+ expect(parsed).toEqual({
106
+ buildInstructions: [
107
+ {
108
+ sourceEntityId: "b2",
109
+ sourceID: undefined,
110
+ materializedTableId: "mt-2",
111
+ physicalTableName: "downstream_v1",
112
+ realization: "COPY",
113
+ },
114
+ ],
115
+ referenceManifest: [
116
+ { sourceEntityId: "b1", physicalTableName: "upstream_table" },
117
+ ],
118
+ strictUpstreams: true,
119
+ });
120
+ });
121
+
122
+ it("rejects a referenceManifest entry missing a required field", async () => {
123
+ const { controller } = build();
124
+ await expect(
125
+ controller.createMaterialization("env", "pkg", {
126
+ buildInstructions: {
127
+ sources: [
128
+ {
129
+ sourceEntityId: "b2",
130
+ materializedTableId: "mt-2",
131
+ physicalTableName: "downstream_v1",
132
+ realization: "COPY",
133
+ },
134
+ ],
135
+ referenceManifest: [{ sourceEntityId: "b1" }],
136
+ },
137
+ }),
138
+ ).rejects.toThrow(BadRequestError);
139
+ });
140
+
141
+ it("rejects a non-boolean strictUpstreams", async () => {
142
+ const { controller } = build();
143
+ await expect(
144
+ controller.createMaterialization("env", "pkg", {
145
+ buildInstructions: {
146
+ sources: [
147
+ {
148
+ sourceEntityId: "b2",
149
+ materializedTableId: "mt-2",
150
+ physicalTableName: "downstream_v1",
151
+ realization: "COPY",
152
+ },
153
+ ],
154
+ strictUpstreams: "yes",
155
+ },
156
+ }),
157
+ ).rejects.toThrow(BadRequestError);
158
+ });
159
+
88
160
  it("rejects buildInstructions without a non-empty sources array", async () => {
89
161
  const { controller } = build();
90
162
  await expect(
@@ -1,5 +1,8 @@
1
1
  import { BadRequestError } from "../errors";
2
- import { BuildInstruction } from "../storage/DatabaseInterface";
2
+ import {
3
+ BuildInstruction,
4
+ ManifestReference,
5
+ } from "../storage/DatabaseInterface";
3
6
  import { MaterializationService } from "../service/materialization_service";
4
7
 
5
8
  export class MaterializationController {
@@ -21,19 +24,28 @@ export class MaterializationController {
21
24
  forceRefresh?: boolean;
22
25
  sourceNames?: string[];
23
26
  buildInstructions?: BuildInstruction[];
27
+ referenceManifest?: ManifestReference[];
28
+ strictUpstreams?: boolean;
24
29
  } {
25
30
  const result: {
26
31
  forceRefresh?: boolean;
27
32
  sourceNames?: string[];
28
33
  buildInstructions?: BuildInstruction[];
34
+ referenceManifest?: ManifestReference[];
35
+ strictUpstreams?: boolean;
29
36
  } = {};
30
37
  if (
31
38
  body.buildInstructions !== undefined &&
32
39
  body.buildInstructions !== null
33
40
  ) {
34
- result.buildInstructions = this.validateBuildInstructions(
35
- body.buildInstructions,
36
- );
41
+ const parsed = this.validateBuildInstructions(body.buildInstructions);
42
+ result.buildInstructions = parsed.sources;
43
+ if (parsed.referenceManifest !== undefined) {
44
+ result.referenceManifest = parsed.referenceManifest;
45
+ }
46
+ if (parsed.strictUpstreams !== undefined) {
47
+ result.strictUpstreams = parsed.strictUpstreams;
48
+ }
37
49
  }
38
50
  if (body.forceRefresh !== undefined) {
39
51
  if (typeof body.forceRefresh !== "boolean") {
@@ -57,22 +69,74 @@ export class MaterializationController {
57
69
 
58
70
  /**
59
71
  * Validate the orchestrated `buildInstructions` payload (BuildInstructions:
60
- * `{ sources: BuildInstruction[] }`) and flatten it to the instruction list
61
- * the service consumes.
72
+ * `{ sources: BuildInstruction[], referenceManifest?, strictUpstreams? }`)
73
+ * into the parts the service consumes: the flattened instruction list, the
74
+ * optional upstream reference manifest, and the strict flag.
62
75
  */
63
- private validateBuildInstructions(raw: unknown): BuildInstruction[] {
76
+ private validateBuildInstructions(raw: unknown): {
77
+ sources: BuildInstruction[];
78
+ referenceManifest?: ManifestReference[];
79
+ strictUpstreams?: boolean;
80
+ } {
64
81
  if (typeof raw !== "object" || raw === null) {
65
82
  throw new BadRequestError("buildInstructions must be an object");
66
83
  }
67
- const sources = (raw as Record<string, unknown>).sources;
84
+ const obj = raw as Record<string, unknown>;
85
+ const sources = obj.sources;
68
86
  if (!Array.isArray(sources) || sources.length === 0) {
69
87
  throw new BadRequestError(
70
88
  "buildInstructions requires a non-empty 'sources' array of BuildInstruction",
71
89
  );
72
90
  }
73
- return sources.map((instruction) =>
74
- this.validateInstruction(instruction),
75
- );
91
+ const result: {
92
+ sources: BuildInstruction[];
93
+ referenceManifest?: ManifestReference[];
94
+ strictUpstreams?: boolean;
95
+ } = {
96
+ sources: sources.map((instruction) =>
97
+ this.validateInstruction(instruction),
98
+ ),
99
+ };
100
+ if (
101
+ obj.referenceManifest !== undefined &&
102
+ obj.referenceManifest !== null
103
+ ) {
104
+ if (!Array.isArray(obj.referenceManifest)) {
105
+ throw new BadRequestError(
106
+ "buildInstructions.referenceManifest must be an array of ManifestReference",
107
+ );
108
+ }
109
+ result.referenceManifest = obj.referenceManifest.map((ref) =>
110
+ this.validateManifestReference(ref),
111
+ );
112
+ }
113
+ if (obj.strictUpstreams !== undefined) {
114
+ if (typeof obj.strictUpstreams !== "boolean") {
115
+ throw new BadRequestError(
116
+ "buildInstructions.strictUpstreams must be a boolean",
117
+ );
118
+ }
119
+ result.strictUpstreams = obj.strictUpstreams;
120
+ }
121
+ return result;
122
+ }
123
+
124
+ private validateManifestReference(raw: unknown): ManifestReference {
125
+ if (typeof raw !== "object" || raw === null) {
126
+ throw new BadRequestError("Each manifest reference must be an object");
127
+ }
128
+ const ref = raw as Record<string, unknown>;
129
+ for (const field of ["sourceEntityId", "physicalTableName"] as const) {
130
+ if (typeof ref[field] !== "string") {
131
+ throw new BadRequestError(
132
+ `Manifest reference is missing required string field '${field}'`,
133
+ );
134
+ }
135
+ }
136
+ return {
137
+ sourceEntityId: ref.sourceEntityId as string,
138
+ physicalTableName: ref.physicalTableName as string,
139
+ };
76
140
  }
77
141
 
78
142
  private validateInstruction(raw: unknown): BuildInstruction {