@abloatai/humans 0.58.0 → 0.59.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.
- package/dist/local/Model.js +10 -1
- package/dist/local/client/schemaConfig.js +1 -0
- package/dist/local/interfaces/index.d.ts +5 -0
- package/dist/local/sync/BootstrapFetcher.js +4 -4
- package/dist/local/sync/schemaDrift.d.ts +15 -1
- package/dist/local/sync/schemaDrift.js +30 -13
- package/package.json +2 -2
- package/src/local/Model.ts +12 -1
- package/src/local/client/schemaConfig.ts +3 -0
- package/src/local/interfaces/index.ts +2 -0
- package/src/local/sync/BootstrapFetcher.ts +5 -4
- package/src/local/sync/schemaDrift.ts +32 -16
package/dist/local/Model.js
CHANGED
|
@@ -189,7 +189,16 @@ export class Model {
|
|
|
189
189
|
* Track property changes
|
|
190
190
|
*/
|
|
191
191
|
propertyChanged(propertyName, oldValue, newValue) {
|
|
192
|
-
|
|
192
|
+
// `createdAt` and `updatedAt` are server-managed bookkeeping, not
|
|
193
|
+
// user-authored model changes. In particular, every real field change
|
|
194
|
+
// advances `updatedAt` below. When a schema explicitly declares that
|
|
195
|
+
// timestamp, MobX observes the assignment and calls this method again;
|
|
196
|
+
// treating that callback as another edit recursively stamps `updatedAt`
|
|
197
|
+
// until the stack overflows. Ignore both timestamps at this boundary so
|
|
198
|
+
// they remain observable without entering the mutation payload.
|
|
199
|
+
if (oldValue === newValue ||
|
|
200
|
+
propertyName === 'createdAt' ||
|
|
201
|
+
propertyName === 'updatedAt')
|
|
193
202
|
return;
|
|
194
203
|
runInAction(() => {
|
|
195
204
|
// Preserve the earliest captured `old` for this field until the entry
|
|
@@ -176,6 +176,7 @@ export function deriveConfigFromSchema(schema) {
|
|
|
176
176
|
// the client compares only the models it declares, so an additive server
|
|
177
177
|
// change stays silent and a real divergence names the exact models.
|
|
178
178
|
expectedModelHashes: Object.fromEntries(Object.entries(toSchemaJSON(schema).models).map(([key, model]) => [key, modelHash(model)])),
|
|
179
|
+
expectedModelShapes: Object.fromEntries(Object.entries(toSchemaJSON(schema).models).map(([key, model]) => [key, Object.fromEntries(Object.entries(model.fields).map(([field, meta]) => [field, { type: meta.type, isOptional: meta.isOptional }]))])),
|
|
179
180
|
// For a projection (`selectModels`/`omitModels`), also carry the full source
|
|
180
181
|
// schema's hash. The drift check accepts a server match on either hash, so a
|
|
181
182
|
// subset client stays quiet against a server running its full source schema.
|
|
@@ -289,6 +289,11 @@ export interface RuntimeConfig {
|
|
|
289
289
|
* Advisory, like the hashes above.
|
|
290
290
|
*/
|
|
291
291
|
expectedModelHashes?: Readonly<Record<string, string>>;
|
|
292
|
+
/** Field shapes paired with expectedModelHashes so drift can name direction, not just a model. */
|
|
293
|
+
expectedModelShapes?: Readonly<Record<string, Readonly<Record<string, {
|
|
294
|
+
readonly type: string;
|
|
295
|
+
readonly isOptional: boolean;
|
|
296
|
+
}>>>>;
|
|
292
297
|
}
|
|
293
298
|
/**
|
|
294
299
|
* Extends the WebSocket event map with your own collaboration events, such as
|
|
@@ -161,14 +161,14 @@ export class BootstrapFetcher {
|
|
|
161
161
|
// network hiccup). Fire-and-forget: never blocks or fails the bootstrap.
|
|
162
162
|
const clientModels = this.runtime.config.expectedModelHashes;
|
|
163
163
|
if (clientModels && Object.keys(clientModels).length > 0) {
|
|
164
|
-
void this.resolveSemanticDrift(clientModels, clientHash, serverHash, where);
|
|
164
|
+
void this.resolveSemanticDrift(clientModels, clientHash, serverHash, where, this.runtime.config.expectedModelShapes);
|
|
165
165
|
return;
|
|
166
166
|
}
|
|
167
167
|
this.warnWholeHashDrift(clientHash, serverHash, where);
|
|
168
168
|
}
|
|
169
169
|
/** Fetch the server's per-model schema surface and warn precisely — or stay
|
|
170
170
|
* silent when every model this client declares matches (additive lead). */
|
|
171
|
-
async resolveSemanticDrift(clientModels, clientHash, serverHash, where) {
|
|
171
|
+
async resolveSemanticDrift(clientModels, clientHash, serverHash, where, clientShapes) {
|
|
172
172
|
try {
|
|
173
173
|
const res = await fetch(`${this.options.baseUrl}/schema`, {
|
|
174
174
|
method: 'GET',
|
|
@@ -181,11 +181,11 @@ export class BootstrapFetcher {
|
|
|
181
181
|
? body.models.flatMap((m) => {
|
|
182
182
|
const entry = m;
|
|
183
183
|
return typeof entry.key === 'string'
|
|
184
|
-
? [{ key: entry.key, ...(typeof entry.hash === 'string' ? { hash: entry.hash } : {}) }]
|
|
184
|
+
? [{ key: entry.key, ...(typeof entry.hash === 'string' ? { hash: entry.hash } : {}), ...(entry.fields && typeof entry.fields === 'object' ? { fields: entry.fields } : {}) }]
|
|
185
185
|
: [];
|
|
186
186
|
})
|
|
187
187
|
: [];
|
|
188
|
-
const finding = classifySchemaDrift(clientModels, models);
|
|
188
|
+
const finding = classifySchemaDrift(clientModels, models, clientShapes);
|
|
189
189
|
if (finding.kind === 'aligned')
|
|
190
190
|
return; // additive server lead — not this client's concern
|
|
191
191
|
if (finding.kind !== 'unknown') {
|
|
@@ -19,6 +19,16 @@ export interface ServerSchemaModel {
|
|
|
19
19
|
readonly key: string;
|
|
20
20
|
/** Per-model content hash; absent on servers older than this check. */
|
|
21
21
|
readonly hash?: string;
|
|
22
|
+
readonly fields?: Readonly<Record<string, {
|
|
23
|
+
readonly type: string;
|
|
24
|
+
readonly isOptional: boolean;
|
|
25
|
+
}>>;
|
|
26
|
+
}
|
|
27
|
+
export interface SchemaFieldDrift {
|
|
28
|
+
readonly model: string;
|
|
29
|
+
readonly field: string;
|
|
30
|
+
readonly direction: 'client_only' | 'active_only' | 'changed';
|
|
31
|
+
readonly detail: string;
|
|
22
32
|
}
|
|
23
33
|
export type SchemaDriftFinding =
|
|
24
34
|
/** Every model this client declares exists server-side with matching content
|
|
@@ -38,13 +48,17 @@ export type SchemaDriftFinding =
|
|
|
38
48
|
readonly kind: 'changed';
|
|
39
49
|
readonly models: readonly string[];
|
|
40
50
|
readonly unpushed: readonly string[];
|
|
51
|
+
readonly fields?: readonly SchemaFieldDrift[];
|
|
41
52
|
}
|
|
42
53
|
/** The server surface carries no per-model hashes (older server) — the
|
|
43
54
|
* caller falls back to the whole-hash comparison. */
|
|
44
55
|
| {
|
|
45
56
|
readonly kind: 'unknown';
|
|
46
57
|
};
|
|
47
|
-
export declare function classifySchemaDrift(clientModels: Readonly<Record<string, string>>, serverModels: readonly ServerSchemaModel[]
|
|
58
|
+
export declare function classifySchemaDrift(clientModels: Readonly<Record<string, string>>, serverModels: readonly ServerSchemaModel[], clientShapes?: Readonly<Record<string, Readonly<Record<string, {
|
|
59
|
+
readonly type: string;
|
|
60
|
+
readonly isOptional: boolean;
|
|
61
|
+
}>>>>): SchemaDriftFinding;
|
|
48
62
|
/**
|
|
49
63
|
* The warning for a real, named divergence. Calm and specific: which models,
|
|
50
64
|
* what that means for this client, and the one next step. Never speaks about
|
|
@@ -14,22 +14,38 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Pure and transport-free; the BootstrapFetcher owns fetching the surface.
|
|
16
16
|
*/
|
|
17
|
-
|
|
17
|
+
import { reconcileClientToActive } from '@abloatai/transaction/schema';
|
|
18
|
+
export function classifySchemaDrift(clientModels, serverModels, clientShapes = {}) {
|
|
18
19
|
if (serverModels.length > 0 && serverModels.every((m) => !m.hash)) {
|
|
19
20
|
return { kind: 'unknown' };
|
|
20
21
|
}
|
|
21
22
|
const server = new Map(serverModels.map((m) => [m.key, m.hash]));
|
|
22
23
|
const unpushed = [];
|
|
23
24
|
const changed = [];
|
|
25
|
+
const fields = [];
|
|
24
26
|
for (const [key, hash] of Object.entries(clientModels)) {
|
|
25
27
|
const serverHash = server.get(key);
|
|
26
28
|
if (serverHash === undefined)
|
|
27
29
|
unpushed.push(key);
|
|
28
|
-
else if (serverHash !== hash)
|
|
30
|
+
else if (serverHash !== hash) {
|
|
29
31
|
changed.push(key);
|
|
32
|
+
const clientFields = clientShapes[key];
|
|
33
|
+
const activeFields = serverModels.find((model) => model.key === key)?.fields;
|
|
34
|
+
if (clientFields && activeFields)
|
|
35
|
+
for (const field of new Set([...Object.keys(clientFields), ...Object.keys(activeFields)])) {
|
|
36
|
+
const client = clientFields[field];
|
|
37
|
+
const active = activeFields[field];
|
|
38
|
+
if (!active)
|
|
39
|
+
fields.push({ model: key, field, direction: 'client_only', detail: 'present in this build but absent from the active schema' });
|
|
40
|
+
else if (!client)
|
|
41
|
+
fields.push({ model: key, field, direction: 'active_only', detail: 'present in the active schema but absent from this build' });
|
|
42
|
+
else if (client.type !== active.type || client.isOptional !== active.isOptional)
|
|
43
|
+
fields.push({ model: key, field, direction: 'changed', detail: `${client.type}${client.isOptional ? ' optional' : ' required'} in this build and ${active.type}${active.isOptional ? ' optional' : ' required'} in the active schema` });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
30
46
|
}
|
|
31
47
|
if (changed.length > 0)
|
|
32
|
-
return { kind: 'changed', models: changed, unpushed };
|
|
48
|
+
return { kind: 'changed', models: changed, unpushed, ...(fields.length ? { fields } : {}) };
|
|
33
49
|
if (unpushed.length > 0)
|
|
34
50
|
return { kind: 'unpushed', models: unpushed };
|
|
35
51
|
return { kind: 'aligned' };
|
|
@@ -40,14 +56,15 @@ export function classifySchemaDrift(clientModels, serverModels) {
|
|
|
40
56
|
* hashes — the point of the semantic check is that nobody has to compare hex.
|
|
41
57
|
*/
|
|
42
58
|
export function describeSchemaDrift(finding, serverLabel) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
`
|
|
52
|
-
|
|
59
|
+
const findings = finding.kind === 'unpushed'
|
|
60
|
+
? reconcileClientToActive([], finding.models, serverLabel)
|
|
61
|
+
: reconcileClientToActive(finding.models, finding.unpushed, serverLabel, finding.fields ?? []);
|
|
62
|
+
const changed = findings.filter(({ code }) => code === 'model_changed').map(({ model }) => model).filter(Boolean);
|
|
63
|
+
const unpushed = findings.filter(({ code }) => code === 'model_unpushed').map(({ model }) => model).filter(Boolean);
|
|
64
|
+
const summary = [
|
|
65
|
+
...findings.filter(({ field }) => field !== undefined).map(({ message }) => message),
|
|
66
|
+
...(changed.length ? [`Models ${changed.join(', ')} differ between this build and the active schema at ${serverLabel}.`] : []),
|
|
67
|
+
...(unpushed.length ? [`Models ${unpushed.join(', ')} are declared by this build but are not active at ${serverLabel}.`] : []),
|
|
68
|
+
];
|
|
69
|
+
return `Ablo: ${summary.join(' ')} ${findings.map(({ action }) => action).filter((value, index, all) => all.indexOf(value) === index).join(' ')}`;
|
|
53
70
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abloatai/humans",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.59.0",
|
|
4
4
|
"description": "The optional human-facing local-state package for Ablo: presence, live queries, and React bindings.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"directory": "packages/humans"
|
|
85
85
|
},
|
|
86
86
|
"dependencies": {
|
|
87
|
-
"@abloatai/transaction": "^0.
|
|
87
|
+
"@abloatai/transaction": "^0.59.0",
|
|
88
88
|
"mobx": "^6.13.7",
|
|
89
89
|
"uuid": "^11.1.0",
|
|
90
90
|
"zod": "^4.4.3"
|
package/src/local/Model.ts
CHANGED
|
@@ -281,7 +281,18 @@ export abstract class Model {
|
|
|
281
281
|
* Track property changes
|
|
282
282
|
*/
|
|
283
283
|
propertyChanged(propertyName: string, oldValue: unknown, newValue: unknown): void {
|
|
284
|
-
|
|
284
|
+
// `createdAt` and `updatedAt` are server-managed bookkeeping, not
|
|
285
|
+
// user-authored model changes. In particular, every real field change
|
|
286
|
+
// advances `updatedAt` below. When a schema explicitly declares that
|
|
287
|
+
// timestamp, MobX observes the assignment and calls this method again;
|
|
288
|
+
// treating that callback as another edit recursively stamps `updatedAt`
|
|
289
|
+
// until the stack overflows. Ignore both timestamps at this boundary so
|
|
290
|
+
// they remain observable without entering the mutation payload.
|
|
291
|
+
if (
|
|
292
|
+
oldValue === newValue ||
|
|
293
|
+
propertyName === 'createdAt' ||
|
|
294
|
+
propertyName === 'updatedAt'
|
|
295
|
+
) return;
|
|
285
296
|
|
|
286
297
|
runInAction(() => {
|
|
287
298
|
// Preserve the earliest captured `old` for this field until the entry
|
|
@@ -185,6 +185,9 @@ export function deriveConfigFromSchema(schema: Schema): RuntimeConfig {
|
|
|
185
185
|
expectedModelHashes: Object.fromEntries(
|
|
186
186
|
Object.entries(toSchemaJSON(schema).models).map(([key, model]) => [key, modelHash(model)]),
|
|
187
187
|
),
|
|
188
|
+
expectedModelShapes: Object.fromEntries(
|
|
189
|
+
Object.entries(toSchemaJSON(schema).models).map(([key, model]) => [key, Object.fromEntries(Object.entries(model.fields).map(([field, meta]) => [field, { type: meta.type, isOptional: meta.isOptional }]))]),
|
|
190
|
+
),
|
|
188
191
|
// For a projection (`selectModels`/`omitModels`), also carry the full source
|
|
189
192
|
// schema's hash. The drift check accepts a server match on either hash, so a
|
|
190
193
|
// subset client stays quiet against a server running its full source schema.
|
|
@@ -436,6 +436,8 @@ export interface RuntimeConfig {
|
|
|
436
436
|
* Advisory, like the hashes above.
|
|
437
437
|
*/
|
|
438
438
|
expectedModelHashes?: Readonly<Record<string, string>>;
|
|
439
|
+
/** Field shapes paired with expectedModelHashes so drift can name direction, not just a model. */
|
|
440
|
+
expectedModelShapes?: Readonly<Record<string, Readonly<Record<string, { readonly type: string; readonly isOptional: boolean }>>>>;
|
|
439
441
|
}
|
|
440
442
|
|
|
441
443
|
// ─────────────────────────────────────────────
|
|
@@ -293,7 +293,7 @@ export class BootstrapFetcher {
|
|
|
293
293
|
// network hiccup). Fire-and-forget: never blocks or fails the bootstrap.
|
|
294
294
|
const clientModels = this.runtime.config.expectedModelHashes;
|
|
295
295
|
if (clientModels && Object.keys(clientModels).length > 0) {
|
|
296
|
-
void this.resolveSemanticDrift(clientModels, clientHash, serverHash, where);
|
|
296
|
+
void this.resolveSemanticDrift(clientModels, clientHash, serverHash, where, this.runtime.config.expectedModelShapes);
|
|
297
297
|
return;
|
|
298
298
|
}
|
|
299
299
|
this.warnWholeHashDrift(clientHash, serverHash, where);
|
|
@@ -306,6 +306,7 @@ export class BootstrapFetcher {
|
|
|
306
306
|
clientHash: string,
|
|
307
307
|
serverHash: string,
|
|
308
308
|
where: string,
|
|
309
|
+
clientShapes: NonNullable<typeof this.runtime.config.expectedModelShapes> | undefined,
|
|
309
310
|
): Promise<void> {
|
|
310
311
|
try {
|
|
311
312
|
const res = await fetch(`${this.options.baseUrl}/schema`, {
|
|
@@ -316,13 +317,13 @@ export class BootstrapFetcher {
|
|
|
316
317
|
const body = (await res.json()) as { models?: unknown };
|
|
317
318
|
const models = Array.isArray(body.models)
|
|
318
319
|
? body.models.flatMap((m): ServerSchemaModel[] => {
|
|
319
|
-
const entry = m as { key?: unknown; hash?: unknown };
|
|
320
|
+
const entry = m as { key?: unknown; hash?: unknown; fields?: unknown };
|
|
320
321
|
return typeof entry.key === 'string'
|
|
321
|
-
? [{ key: entry.key, ...(typeof entry.hash === 'string' ? { hash: entry.hash } : {}) }]
|
|
322
|
+
? [{ key: entry.key, ...(typeof entry.hash === 'string' ? { hash: entry.hash } : {}), ...(entry.fields && typeof entry.fields === 'object' ? { fields: entry.fields as ServerSchemaModel['fields'] } : {}) }]
|
|
322
323
|
: [];
|
|
323
324
|
})
|
|
324
325
|
: [];
|
|
325
|
-
const finding = classifySchemaDrift(clientModels, models);
|
|
326
|
+
const finding = classifySchemaDrift(clientModels, models, clientShapes);
|
|
326
327
|
if (finding.kind === 'aligned') return; // additive server lead — not this client's concern
|
|
327
328
|
if (finding.kind !== 'unknown') {
|
|
328
329
|
this.runtime.logger.warn(describeSchemaDrift(finding, where), {
|
|
@@ -15,13 +15,18 @@
|
|
|
15
15
|
* Pure and transport-free; the BootstrapFetcher owns fetching the surface.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { reconcileClientToActive } from '@abloatai/transaction/schema';
|
|
19
|
+
|
|
18
20
|
/** One model as the server's schema read-back reports it. */
|
|
19
21
|
export interface ServerSchemaModel {
|
|
20
22
|
readonly key: string;
|
|
21
23
|
/** Per-model content hash; absent on servers older than this check. */
|
|
22
24
|
readonly hash?: string;
|
|
25
|
+
readonly fields?: Readonly<Record<string, { readonly type: string; readonly isOptional: boolean }>>;
|
|
23
26
|
}
|
|
24
27
|
|
|
28
|
+
export interface SchemaFieldDrift { readonly model: string; readonly field: string; readonly direction: 'client_only' | 'active_only' | 'changed'; readonly detail: string; }
|
|
29
|
+
|
|
25
30
|
export type SchemaDriftFinding =
|
|
26
31
|
/** Every model this client declares exists server-side with matching content
|
|
27
32
|
* (the server may know more — that's an additive lead, not drift). */
|
|
@@ -35,6 +40,7 @@ export type SchemaDriftFinding =
|
|
|
35
40
|
readonly kind: 'changed';
|
|
36
41
|
readonly models: readonly string[];
|
|
37
42
|
readonly unpushed: readonly string[];
|
|
43
|
+
readonly fields?: readonly SchemaFieldDrift[];
|
|
38
44
|
}
|
|
39
45
|
/** The server surface carries no per-model hashes (older server) — the
|
|
40
46
|
* caller falls back to the whole-hash comparison. */
|
|
@@ -43,6 +49,7 @@ export type SchemaDriftFinding =
|
|
|
43
49
|
export function classifySchemaDrift(
|
|
44
50
|
clientModels: Readonly<Record<string, string>>,
|
|
45
51
|
serverModels: readonly ServerSchemaModel[],
|
|
52
|
+
clientShapes: Readonly<Record<string, Readonly<Record<string, { readonly type: string; readonly isOptional: boolean }>>>> = {},
|
|
46
53
|
): SchemaDriftFinding {
|
|
47
54
|
if (serverModels.length > 0 && serverModels.every((m) => !m.hash)) {
|
|
48
55
|
return { kind: 'unknown' };
|
|
@@ -50,12 +57,24 @@ export function classifySchemaDrift(
|
|
|
50
57
|
const server = new Map(serverModels.map((m) => [m.key, m.hash]));
|
|
51
58
|
const unpushed: string[] = [];
|
|
52
59
|
const changed: string[] = [];
|
|
60
|
+
const fields: SchemaFieldDrift[] = [];
|
|
53
61
|
for (const [key, hash] of Object.entries(clientModels)) {
|
|
54
62
|
const serverHash = server.get(key);
|
|
55
63
|
if (serverHash === undefined) unpushed.push(key);
|
|
56
|
-
else if (serverHash !== hash)
|
|
64
|
+
else if (serverHash !== hash) {
|
|
65
|
+
changed.push(key);
|
|
66
|
+
const clientFields = clientShapes[key];
|
|
67
|
+
const activeFields = serverModels.find((model) => model.key === key)?.fields;
|
|
68
|
+
if (clientFields && activeFields) for (const field of new Set([...Object.keys(clientFields), ...Object.keys(activeFields)])) {
|
|
69
|
+
const client = clientFields[field];
|
|
70
|
+
const active = activeFields[field];
|
|
71
|
+
if (!active) fields.push({ model: key, field, direction: 'client_only', detail: 'present in this build but absent from the active schema' });
|
|
72
|
+
else if (!client) fields.push({ model: key, field, direction: 'active_only', detail: 'present in the active schema but absent from this build' });
|
|
73
|
+
else if (client.type !== active.type || client.isOptional !== active.isOptional) fields.push({ model: key, field, direction: 'changed', detail: `${client.type}${client.isOptional ? ' optional' : ' required'} in this build and ${active.type}${active.isOptional ? ' optional' : ' required'} in the active schema` });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
57
76
|
}
|
|
58
|
-
if (changed.length > 0) return { kind: 'changed', models: changed, unpushed };
|
|
77
|
+
if (changed.length > 0) return { kind: 'changed', models: changed, unpushed, ...(fields.length ? { fields } : {}) };
|
|
59
78
|
if (unpushed.length > 0) return { kind: 'unpushed', models: unpushed };
|
|
60
79
|
return { kind: 'aligned' };
|
|
61
80
|
}
|
|
@@ -69,18 +88,15 @@ export function describeSchemaDrift(
|
|
|
69
88
|
finding: Extract<SchemaDriftFinding, { kind: 'unpushed' | 'changed' }>,
|
|
70
89
|
serverLabel: string,
|
|
71
90
|
): string {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
`declined — \`ablo status\` shows the deployed shape; pushing your schema or deploying a ` +
|
|
84
|
-
`current build aligns them.`
|
|
85
|
-
);
|
|
91
|
+
const findings = finding.kind === 'unpushed'
|
|
92
|
+
? reconcileClientToActive([], finding.models, serverLabel)
|
|
93
|
+
: reconcileClientToActive(finding.models, finding.unpushed, serverLabel, finding.fields ?? []);
|
|
94
|
+
const changed = findings.filter(({ code }) => code === 'model_changed').map(({ model }) => model).filter(Boolean);
|
|
95
|
+
const unpushed = findings.filter(({ code }) => code === 'model_unpushed').map(({ model }) => model).filter(Boolean);
|
|
96
|
+
const summary = [
|
|
97
|
+
...findings.filter(({ field }) => field !== undefined).map(({ message }) => message),
|
|
98
|
+
...(changed.length ? [`Models ${changed.join(', ')} differ between this build and the active schema at ${serverLabel}.`] : []),
|
|
99
|
+
...(unpushed.length ? [`Models ${unpushed.join(', ')} are declared by this build but are not active at ${serverLabel}.`] : []),
|
|
100
|
+
];
|
|
101
|
+
return `Ablo: ${summary.join(' ')} ${findings.map(({ action }) => action).filter((value, index, all) => all.indexOf(value) === index).join(' ')}`;
|
|
86
102
|
}
|