@butinapp/shapes 0.1.0 → 0.1.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.
@@ -140,6 +140,7 @@ export type ExportBundle = z.infer<typeof ExportBundleSchema>;
140
140
  export type ParseBundleResult = {
141
141
  ok: true;
142
142
  bundle: ExportBundle;
143
+ warnings: string[];
143
144
  } | {
144
145
  ok: false;
145
146
  errors: string[];
@@ -52,6 +52,50 @@ export const ExportBundleSchema = z.object({
52
52
  // renderer ("the cloud is dumb on purpose").
53
53
  overview: z.array(OverviewPluginSchema)
54
54
  });
55
+ // The bundle envelope with its two service arrays left unvalidated (validated per-entry so one bad entry
56
+ // doesn't reject the whole blob). Scalar top-level fields still fail hard.
57
+ const ExportBundleEnvelopeSchema = ExportBundleSchema.extend({
58
+ plugins: z.array(z.unknown()),
59
+ overview: z.array(z.unknown())
60
+ });
61
+ // Walk a value along a zod issue path, returning what the issue points at (undefined if the path runs off a
62
+ // null/absent branch) — lets us tell an absent field apart from a present-but-wrong one.
63
+ const valueAtPath = (value, path) => path.reduce((acc, key) => (acc == null ? undefined : acc[key]), value);
64
+ const serviceOf = (kind, item) => {
65
+ if (kind === 'overview') {
66
+ const o = item;
67
+ return o?.pluginId ?? o?.pluginName;
68
+ }
69
+ return item?.meta?.id;
70
+ };
71
+ // Validate one array's entries independently: keep the ones that parse, and turn each failure into a warning
72
+ // that names the service and flags absent fields (which usually mean the entry predates a schema addition).
73
+ const salvageEntries = (schema, items, kind) => {
74
+ const kept = [];
75
+ const warnings = [];
76
+ items.forEach((item, index) => {
77
+ const parsed = schema.safeParse(item);
78
+ if (parsed.success) {
79
+ kept.push(parsed.data);
80
+ return;
81
+ }
82
+ const service = serviceOf(kind, item);
83
+ const label = `${kind}[${index}]${service ? ` (service '${service}')` : ''}`;
84
+ let anyAbsent = false;
85
+ const detail = parsed.error.issues
86
+ .map((i) => {
87
+ const absent = valueAtPath(item, i.path) === undefined;
88
+ anyAbsent ||= absent;
89
+ return `${i.path.join('.') || '(value)'}: ${i.message}`;
90
+ })
91
+ .join('; ');
92
+ const hint = anyAbsent
93
+ ? " — a field is absent (this service's cached data predates it); refresh it and re-export"
94
+ : '';
95
+ warnings.push(`dropped ${label}: ${detail}${hint}`);
96
+ });
97
+ return { kept, warnings };
98
+ };
55
99
  export const parseExportBundle = (raw) => {
56
100
  const version = raw?.formatVersion;
57
101
  if (typeof version === 'number' && version > EXPORT_BUNDLE_FORMAT_VERSION) {
@@ -60,9 +104,15 @@ export const parseExportBundle = (raw) => {
60
104
  errors: [`bundle formatVersion ${version} is newer than this viewer supports (${EXPORT_BUNDLE_FORMAT_VERSION})`]
61
105
  };
62
106
  }
63
- const parsed = ExportBundleSchema.safeParse(raw);
64
- if (!parsed.success) {
65
- return { ok: false, errors: parsed.error.issues.map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`) };
107
+ const envelope = ExportBundleEnvelopeSchema.safeParse(raw);
108
+ if (!envelope.success) {
109
+ return { ok: false, errors: envelope.error.issues.map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`) };
66
110
  }
67
- return { ok: true, bundle: parsed.data };
111
+ const plugins = salvageEntries(ExportPluginSchema, envelope.data.plugins, 'plugins');
112
+ const overview = salvageEntries(OverviewPluginSchema, envelope.data.overview, 'overview');
113
+ return {
114
+ ok: true,
115
+ bundle: { ...envelope.data, plugins: plugins.kept, overview: overview.kept },
116
+ warnings: [...plugins.warnings, ...overview.warnings]
117
+ };
68
118
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@butinapp/shapes",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Butin's host-layer data shapes built on the @butinapp/sdk data-view contract: the cross-service Overview input, the persisted ledger, the presentation manifest, and the portable export-bundle wire format.",
5
5
  "homepage": "https://butin.app",
6
6
  "bugs": "https://github.com/butinapp/butin/issues",
@@ -37,7 +37,7 @@
37
37
  "lodash-es": "^4.18.1",
38
38
  "luxon": "^3.7.2",
39
39
  "zod": "^4.3.6",
40
- "@butinapp/sdk": "0.1.0"
40
+ "@butinapp/sdk": "0.1.1"
41
41
  },
42
42
  "devDependencies": {
43
43
  "vitest": "^4.1.7"