busa-cli 0.19.1 → 0.19.2
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/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{run--bA-jLSP.js → run-ZDakuDNP.js} +240 -110
- package/package.json +2 -2
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { n as runCli, r as render, t as HELP } from "./run
|
|
1
|
+
import { n as runCli, r as render, t as HELP } from "./run-ZDakuDNP.js";
|
|
2
2
|
import { DEFAULT_BASE_URL, createBusabaseClient, normalizeBaseUrl } from "busabase-sdk";
|
|
3
3
|
export { DEFAULT_BASE_URL, HELP, createBusabaseClient, normalizeBaseUrl, render, runCli };
|
|
@@ -5301,14 +5301,14 @@ z.object({
|
|
|
5301
5301
|
page: 1,
|
|
5302
5302
|
pageSize: 50
|
|
5303
5303
|
});
|
|
5304
|
-
z.object({
|
|
5304
|
+
const listChangeRequestsPageResponseSchema = z.object({
|
|
5305
5305
|
changeRequests: z.array(changeRequestSchema),
|
|
5306
5306
|
total: z.number().int().nonnegative(),
|
|
5307
5307
|
totalPages: z.number().int().nonnegative(),
|
|
5308
5308
|
page: z.number().int().min(1),
|
|
5309
5309
|
pageSize: z.number().int().min(1).max(100)
|
|
5310
5310
|
});
|
|
5311
|
-
z.object({
|
|
5311
|
+
const changeRequestCountsSchema = z.object({
|
|
5312
5312
|
review: z.number().int().nonnegative(),
|
|
5313
5313
|
changes: z.number().int().nonnegative(),
|
|
5314
5314
|
created: z.number().int().nonnegative(),
|
|
@@ -5316,6 +5316,7 @@ z.object({
|
|
|
5316
5316
|
merged: z.number().int().nonnegative(),
|
|
5317
5317
|
rejected: z.number().int().nonnegative()
|
|
5318
5318
|
});
|
|
5319
|
+
listChangeRequestsPageResponseSchema.extend({ counts: changeRequestCountsSchema });
|
|
5319
5320
|
const SEARCH_SOURCES = [
|
|
5320
5321
|
"records",
|
|
5321
5322
|
"files",
|
|
@@ -5994,8 +5995,75 @@ const reportUnresolvableDocAssets = (slug, body, carried = /* @__PURE__ */ new S
|
|
|
5994
5995
|
if (unresolved.length === 0) return void 0;
|
|
5995
5996
|
return `Doc "${slug}" embeds ${unresolved.length} image(s) that this package does not carry (${unresolved.join(", ")}). Everything else about the doc is intact, but those images will not render on another host. Re-upload them in the Doc editor, or check that the source host can still serve them.`;
|
|
5996
5997
|
};
|
|
5998
|
+
//#endregion
|
|
5999
|
+
//#region ../../packages/busabase-package/src/record-dependencies.ts
|
|
6000
|
+
const recordIdentity = (baseSlug, recordKey) => `${baseSlug}\u0000${recordKey}`;
|
|
6001
|
+
const toRecordKeyArray = (value) => {
|
|
6002
|
+
if (Array.isArray(value)) return value.filter((item) => typeof item === "string" && item.length > 0);
|
|
6003
|
+
if (typeof value === "string" && value.length > 0) return [value];
|
|
6004
|
+
return [];
|
|
6005
|
+
};
|
|
6006
|
+
const collectBases = (nodes) => {
|
|
6007
|
+
const bases = [];
|
|
6008
|
+
for (const node of nodes) {
|
|
6009
|
+
if (node.type === "base") bases.push(node);
|
|
6010
|
+
if (node.type === "folder") bases.push(...collectBases(node.children));
|
|
6011
|
+
}
|
|
6012
|
+
return bases;
|
|
6013
|
+
};
|
|
6014
|
+
const label = (baseSlug, recordKey) => `${baseSlug}/${recordKey}`;
|
|
6015
|
+
/**
|
|
6016
|
+
* Order sample records so every required relation points at an already-created row.
|
|
6017
|
+
*
|
|
6018
|
+
* Optional relations remain a separate linking pass. Required relations cannot: the
|
|
6019
|
+
* Base rejects a record before that later pass if its required value is absent.
|
|
6020
|
+
*/
|
|
6021
|
+
const buildRecordCreateLayers = (nodes) => {
|
|
6022
|
+
const bases = collectBases(nodes);
|
|
6023
|
+
const basesBySlug = new Map(bases.map((base) => [base.slug, base]));
|
|
6024
|
+
const records = /* @__PURE__ */ new Map();
|
|
6025
|
+
for (const base of bases) for (const record of base.records) records.set(recordIdentity(base.slug, record.key), {
|
|
6026
|
+
base,
|
|
6027
|
+
record
|
|
6028
|
+
});
|
|
6029
|
+
const dependencies = /* @__PURE__ */ new Map();
|
|
6030
|
+
for (const planned of records.values()) {
|
|
6031
|
+
const id = recordIdentity(planned.base.slug, planned.record.key);
|
|
6032
|
+
const required = /* @__PURE__ */ new Set();
|
|
6033
|
+
for (const field of planned.base.base.fields) {
|
|
6034
|
+
if (field.type !== "relation" || !field.required) continue;
|
|
6035
|
+
const targetBaseSlug = field.options.targetBaseSlug;
|
|
6036
|
+
if (!targetBaseSlug || !basesBySlug.has(targetBaseSlug)) throw new Error(`Required relation "${planned.base.slug}.${field.slug}" targets missing Base "${targetBaseSlug || "(unset)"}". Nothing was installed.`);
|
|
6037
|
+
const keys = toRecordKeyArray(planned.record.fields[field.slug]);
|
|
6038
|
+
if (keys.length === 0) throw new Error(`Sample record "${label(planned.base.slug, planned.record.key)}" is missing required relation "${field.slug}". Nothing was installed.`);
|
|
6039
|
+
for (const key of keys) {
|
|
6040
|
+
const dependency = recordIdentity(targetBaseSlug, key);
|
|
6041
|
+
if (!records.has(dependency)) throw new Error(`Sample record "${label(planned.base.slug, planned.record.key)}" requires "${targetBaseSlug}/${key}" through relation "${field.slug}", but that target is not in this package. Nothing was installed.`);
|
|
6042
|
+
required.add(dependency);
|
|
6043
|
+
}
|
|
6044
|
+
}
|
|
6045
|
+
dependencies.set(id, required);
|
|
6046
|
+
}
|
|
6047
|
+
const remaining = new Map(records);
|
|
6048
|
+
const created = /* @__PURE__ */ new Set();
|
|
6049
|
+
const layers = [];
|
|
6050
|
+
while (remaining.size > 0) {
|
|
6051
|
+
const ready = [...remaining.entries()].filter(([id]) => [...dependencies.get(id) ?? []].every((dependency) => created.has(dependency))).sort(([, a], [, b]) => `${a.base.slug}\u0000${a.record.key}`.localeCompare(`${b.base.slug}\u0000${b.record.key}`, "en"));
|
|
6052
|
+
if (ready.length === 0) {
|
|
6053
|
+
const cycle = [...remaining.values()].map((item) => label(item.base.slug, item.record.key)).sort((a, b) => a.localeCompare(b, "en"));
|
|
6054
|
+
throw new Error(`Required sample relations contain a cycle involving: ${cycle.join(", ")}. Required relation targets must be creatable first. Nothing was installed.`);
|
|
6055
|
+
}
|
|
6056
|
+
layers.push(ready.map(([, planned]) => planned));
|
|
6057
|
+
for (const [id] of ready) {
|
|
6058
|
+
remaining.delete(id);
|
|
6059
|
+
created.add(id);
|
|
6060
|
+
}
|
|
6061
|
+
}
|
|
6062
|
+
return layers;
|
|
6063
|
+
};
|
|
5997
6064
|
const fieldKey = (baseSlug, fieldSlug) => `${baseSlug}\u0000${fieldSlug}`;
|
|
5998
6065
|
const applyInstall = async (client, plan, options) => {
|
|
6066
|
+
const recordCreateLayers = buildRecordCreateLayers(plan.tree.nodes);
|
|
5999
6067
|
const state = {
|
|
6000
6068
|
targetFolderNodeId: "",
|
|
6001
6069
|
created: {
|
|
@@ -6014,90 +6082,144 @@ const applyInstall = async (client, plan, options) => {
|
|
|
6014
6082
|
const bases = [];
|
|
6015
6083
|
const progress = options.onProgress ?? (() => {});
|
|
6016
6084
|
const app = resolveAppContext(plan, options);
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
state.targetFolderNodeId =
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
});
|
|
6029
|
-
state.created.folders++;
|
|
6030
|
-
}
|
|
6031
|
-
await createStructure(client, plan.tree.nodes, state.targetFolderNodeId, options, state, bases, fieldIds, docAssetIds, app);
|
|
6032
|
-
if (app?.rootSkill) {
|
|
6033
|
-
progress("Pass 1/5: installing the app skill…");
|
|
6034
|
-
await createFileTreeNode(client, {
|
|
6035
|
-
type: "skill",
|
|
6036
|
-
slug: app.rootSkill.slug,
|
|
6037
|
-
name: app.rootSkill.name,
|
|
6038
|
-
description: app.rootSkill.description,
|
|
6039
|
-
position: void 0,
|
|
6040
|
-
files: app.rootSkill.files
|
|
6041
|
-
}, state.targetFolderNodeId, options, state, app, { [TEMPLATE_SKILL_METADATA_KEY]: true });
|
|
6042
|
-
}
|
|
6043
|
-
const deferred = bases.filter((base) => base.node.base.fields.some(isDeferredField));
|
|
6044
|
-
if (deferred.length > 0) {
|
|
6045
|
-
progress("Pass 2/5: adding relation and AI fields…");
|
|
6046
|
-
for (const base of deferred) for (const field of base.node.base.fields.filter(isDeferredField)) {
|
|
6047
|
-
const updated = await client.bases.createField({
|
|
6048
|
-
baseId: base.baseId,
|
|
6049
|
-
slug: field.slug,
|
|
6050
|
-
name: field.name,
|
|
6051
|
-
type: field.type,
|
|
6052
|
-
required: field.required,
|
|
6053
|
-
options: toApiFieldOptions(field.options)
|
|
6054
|
-
});
|
|
6055
|
-
indexFields(fieldIds, base.node.slug, updated.fields);
|
|
6056
|
-
}
|
|
6057
|
-
}
|
|
6058
|
-
const patchable = bases.flatMap((base) => base.node.base.fields.filter(needsFieldIdPatch).map((field) => ({
|
|
6059
|
-
base,
|
|
6060
|
-
field
|
|
6061
|
-
})));
|
|
6062
|
-
if (patchable.length > 0) {
|
|
6063
|
-
progress("Pass 3/5: resolving inverse and AI source field references…");
|
|
6064
|
-
for (const { base, field } of patchable) {
|
|
6065
|
-
const fieldId = fieldIds.get(fieldKey(base.node.slug, field.slug));
|
|
6066
|
-
if (!fieldId) throw new Error(`Internal error: no field id for ${base.node.slug}.${field.slug} in pass 3.`);
|
|
6067
|
-
const patched = resolveFieldIdOptions(field, base.node.slug, fieldIds, state.warnings);
|
|
6068
|
-
if (!patched) continue;
|
|
6069
|
-
const changeRequest = await client.bases.fieldChangeRequest({
|
|
6070
|
-
operation: "update",
|
|
6071
|
-
baseId: base.baseId,
|
|
6072
|
-
fieldId,
|
|
6073
|
-
patch: { options: patched },
|
|
6074
|
-
message: `Wire up ${base.node.slug}.${field.slug} references`,
|
|
6085
|
+
let phase = "Pass 1/5 (structure)";
|
|
6086
|
+
try {
|
|
6087
|
+
const docAssetIds = await uploadDocAssets(client, plan.tree.assets ?? [], state, progress, options.serverUrl);
|
|
6088
|
+
progress("Pass 1/5: creating the node tree…");
|
|
6089
|
+
if (plan.existingTargetFolderNodeId) state.targetFolderNodeId = plan.existingTargetFolderNodeId;
|
|
6090
|
+
else {
|
|
6091
|
+
state.targetFolderNodeId = await createFolderNode(client, {
|
|
6092
|
+
parentNodeId: void 0,
|
|
6093
|
+
slug: plan.targetFolderSlug,
|
|
6094
|
+
name: plan.tree.manifest.name,
|
|
6095
|
+
description: plan.tree.manifest.description,
|
|
6075
6096
|
submittedBy: options.submittedBy,
|
|
6076
|
-
|
|
6097
|
+
metadata: app?.rootMetadata
|
|
6077
6098
|
});
|
|
6078
|
-
|
|
6099
|
+
state.created.folders++;
|
|
6079
6100
|
}
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6101
|
+
await createStructure(client, plan.tree.nodes, state.targetFolderNodeId, options, state, bases, fieldIds, docAssetIds, app);
|
|
6102
|
+
if (app?.rootSkill) {
|
|
6103
|
+
progress("Pass 1/5: installing the app skill…");
|
|
6104
|
+
await createFileTreeNode(client, {
|
|
6105
|
+
type: "skill",
|
|
6106
|
+
slug: app.rootSkill.slug,
|
|
6107
|
+
name: app.rootSkill.name,
|
|
6108
|
+
description: app.rootSkill.description,
|
|
6109
|
+
position: void 0,
|
|
6110
|
+
files: app.rootSkill.files
|
|
6111
|
+
}, state.targetFolderNodeId, options, state, app, { [TEMPLATE_SKILL_METADATA_KEY]: true });
|
|
6112
|
+
}
|
|
6113
|
+
phase = "Pass 2/5 (relation and AI fields)";
|
|
6114
|
+
const deferred = bases.filter((base) => base.node.base.fields.some(isDeferredField));
|
|
6115
|
+
if (deferred.length > 0) {
|
|
6116
|
+
progress("Pass 2/5: adding relation and AI fields…");
|
|
6117
|
+
for (const base of deferred) for (const field of base.node.base.fields.filter(isDeferredField)) {
|
|
6118
|
+
const updated = await client.bases.createField({
|
|
6119
|
+
baseId: base.baseId,
|
|
6120
|
+
slug: field.slug,
|
|
6121
|
+
name: field.name,
|
|
6122
|
+
type: field.type,
|
|
6123
|
+
required: field.required,
|
|
6124
|
+
options: toApiFieldOptions(field.options)
|
|
6125
|
+
});
|
|
6126
|
+
indexFields(fieldIds, base.node.slug, updated.fields);
|
|
6127
|
+
}
|
|
6128
|
+
}
|
|
6129
|
+
phase = "Pass 3/5 (field reference wiring)";
|
|
6130
|
+
const patchable = bases.flatMap((base) => base.node.base.fields.filter(needsFieldIdPatch).map((field) => ({
|
|
6131
|
+
base,
|
|
6132
|
+
field
|
|
6133
|
+
})));
|
|
6134
|
+
if (patchable.length > 0) {
|
|
6135
|
+
progress("Pass 3/5: resolving inverse and AI source field references…");
|
|
6136
|
+
for (const { base, field } of patchable) {
|
|
6137
|
+
const fieldId = fieldIds.get(fieldKey(base.node.slug, field.slug));
|
|
6138
|
+
if (!fieldId) throw new Error(`Internal error: no field id for ${base.node.slug}.${field.slug} in pass 3.`);
|
|
6139
|
+
const patched = resolveFieldIdOptions(field, base.node.slug, fieldIds, state.warnings);
|
|
6140
|
+
if (!patched) continue;
|
|
6141
|
+
const changeRequest = await client.bases.fieldChangeRequest({
|
|
6142
|
+
operation: "update",
|
|
6143
|
+
baseId: base.baseId,
|
|
6144
|
+
fieldId,
|
|
6145
|
+
patch: { options: patched },
|
|
6146
|
+
message: `Wire up ${base.node.slug}.${field.slug} references`,
|
|
6147
|
+
submittedBy: options.submittedBy,
|
|
6148
|
+
autoMerge: true
|
|
6149
|
+
});
|
|
6150
|
+
if (changeRequest.status !== "merged") throw new Error(`Field reference wiring for ${base.node.slug}.${field.slug} was requested immediately but came back "${changeRequest.status}"; install cannot leave a Base's relation fields unwired.`);
|
|
6151
|
+
}
|
|
6152
|
+
}
|
|
6153
|
+
phase = "Pass 4/5 (sample records)";
|
|
6154
|
+
const recordIdsByKey = /* @__PURE__ */ new Map();
|
|
6155
|
+
if (recordCreateLayers.length > 0) {
|
|
6156
|
+
progress("Pass 4/5: creating records…");
|
|
6157
|
+
const basesBySlug = new Map(bases.map((base) => [base.node.slug, base]));
|
|
6158
|
+
for (const layer of recordCreateLayers) {
|
|
6159
|
+
const byBase = /* @__PURE__ */ new Map();
|
|
6160
|
+
for (const planned of layer) {
|
|
6161
|
+
const group = byBase.get(planned.base.slug) ?? [];
|
|
6162
|
+
group.push(planned);
|
|
6163
|
+
byBase.set(planned.base.slug, group);
|
|
6164
|
+
}
|
|
6165
|
+
for (const [baseSlug, planned] of [...byBase].sort(([a], [b]) => a.localeCompare(b, "en"))) {
|
|
6166
|
+
const base = basesBySlug.get(baseSlug);
|
|
6167
|
+
if (!base) throw new Error(`Internal error: no materialized Base for ${baseSlug} in pass 4.`);
|
|
6168
|
+
await createRecords(client, base, planned.map((item) => item.record), plan, options, state, recordIdsByKey, app);
|
|
6169
|
+
}
|
|
6170
|
+
}
|
|
6171
|
+
}
|
|
6172
|
+
phase = "Pass 5/5 (relation links)";
|
|
6173
|
+
const relationFields = /* @__PURE__ */ new Map();
|
|
6094
6174
|
for (const base of bases) {
|
|
6095
|
-
const
|
|
6096
|
-
if (
|
|
6097
|
-
await linkRelations(client, base, slugs, recordIdsByKey, options, state);
|
|
6175
|
+
const fields = base.node.base.fields.filter((field) => field.type === "relation");
|
|
6176
|
+
if (fields.length > 0) relationFields.set(base.node.slug, fields);
|
|
6098
6177
|
}
|
|
6178
|
+
if (relationFields.size > 0) {
|
|
6179
|
+
progress("Pass 5/5: linking relations…");
|
|
6180
|
+
for (const base of bases) {
|
|
6181
|
+
const fields = relationFields.get(base.node.slug);
|
|
6182
|
+
if (!fields) continue;
|
|
6183
|
+
await linkRelations(client, base, fields, recordIdsByKey, options, state);
|
|
6184
|
+
}
|
|
6185
|
+
}
|
|
6186
|
+
return state;
|
|
6187
|
+
} catch (error) {
|
|
6188
|
+
const created = Object.entries(state.created).filter(([, count]) => count > 0).map(([kind, count]) => `${count} ${kind}`).join(", ");
|
|
6189
|
+
const partial = created ? `Created before failure: ${created}. Target folder: ${plan.targetFolderSlug}.` : "No package resources were created.";
|
|
6190
|
+
const pending = state.pendingChangeRequests ? ` ${state.pendingChangeRequests} ChangeRequest(s) may still be pending.` : "";
|
|
6191
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
6192
|
+
const message = `Install stopped during ${phase}. ${partial}${pending} Cause: ${reason}`;
|
|
6193
|
+
const details = {
|
|
6194
|
+
phase,
|
|
6195
|
+
targetFolderSlug: plan.targetFolderSlug,
|
|
6196
|
+
created: { ...state.created },
|
|
6197
|
+
pendingChangeRequests: state.pendingChangeRequests
|
|
6198
|
+
};
|
|
6199
|
+
if (error instanceof Error) {
|
|
6200
|
+
error.message = message;
|
|
6201
|
+
throw attachInstallFailureDetails(error, details);
|
|
6202
|
+
}
|
|
6203
|
+
throw attachInstallFailureDetails(new Error(message), details);
|
|
6099
6204
|
}
|
|
6100
|
-
|
|
6205
|
+
};
|
|
6206
|
+
/**
|
|
6207
|
+
* A property rather than a subclass: the whole point of the rethrow above is to
|
|
6208
|
+
* keep the original error's prototype (an `ORPCError`'s `code`/`status`/`data`
|
|
6209
|
+
* are what make a permission or quota failure actionable), so the diagnostics
|
|
6210
|
+
* have to ride along on it instead of replacing it.
|
|
6211
|
+
*/
|
|
6212
|
+
const INSTALL_FAILURE_DETAILS_KEY = "busabaseInstallFailure";
|
|
6213
|
+
const attachInstallFailureDetails = (error, details) => {
|
|
6214
|
+
try {
|
|
6215
|
+
Object.defineProperty(error, INSTALL_FAILURE_DETAILS_KEY, {
|
|
6216
|
+
value: details,
|
|
6217
|
+
enumerable: false,
|
|
6218
|
+
configurable: true,
|
|
6219
|
+
writable: true
|
|
6220
|
+
});
|
|
6221
|
+
} catch {}
|
|
6222
|
+
return error;
|
|
6101
6223
|
};
|
|
6102
6224
|
const resolveAppContext = (plan, options) => {
|
|
6103
6225
|
if (!plan.isTemplate) return void 0;
|
|
@@ -6453,28 +6575,39 @@ const resolveFieldIdOptions = (field, baseSlug, fieldIds, warnings) => {
|
|
|
6453
6575
|
const indexFields = (fieldIds, baseSlug, fields) => {
|
|
6454
6576
|
for (const field of fields) fieldIds.set(fieldKey(baseSlug, field.slug), field.id);
|
|
6455
6577
|
};
|
|
6456
|
-
/**
|
|
6457
|
-
|
|
6458
|
-
|
|
6578
|
+
/**
|
|
6579
|
+
* Values the server computes and optional relations are omitted in pass 4.
|
|
6580
|
+
* Required relations are resolved to canonical ids once their dependency layer exists.
|
|
6581
|
+
*/
|
|
6582
|
+
const toCreatableFields = (node, fields, recordIdsByKey) => {
|
|
6583
|
+
const fieldBySlug = new Map(node.base.fields.map((field) => [field.slug, field]));
|
|
6459
6584
|
const result = {};
|
|
6460
6585
|
for (const [slug, value] of Object.entries(fields)) {
|
|
6461
|
-
const
|
|
6462
|
-
if (!
|
|
6463
|
-
if (type === "relation")
|
|
6464
|
-
|
|
6465
|
-
|
|
6586
|
+
const field = fieldBySlug.get(slug);
|
|
6587
|
+
if (!field) continue;
|
|
6588
|
+
if (field.type === "relation") {
|
|
6589
|
+
if (!field.required) continue;
|
|
6590
|
+
const targetBaseSlug = field.options.targetBaseSlug;
|
|
6591
|
+
const keys = toRecordKeyArray(value);
|
|
6592
|
+
const targetIds = keys.map((key) => targetBaseSlug ? recordIdsByKey.get(recordIdentity(targetBaseSlug, key)) : void 0).filter((id) => Boolean(id));
|
|
6593
|
+
if (targetIds.length !== keys.length) throw new Error(`Internal error: required relation ${node.slug}.${slug} was scheduled before its targets existed.`);
|
|
6594
|
+
result[slug] = targetIds;
|
|
6595
|
+
continue;
|
|
6596
|
+
}
|
|
6597
|
+
if (PACKAGE_COMPUTED_FIELD_TYPES.includes(field.type)) continue;
|
|
6598
|
+
if (field.type === "attachment") continue;
|
|
6466
6599
|
result[slug] = value;
|
|
6467
6600
|
}
|
|
6468
6601
|
return result;
|
|
6469
6602
|
};
|
|
6470
|
-
const createRecords = async (client, base, plan, options, state, recordIdsByKey, app) => {
|
|
6603
|
+
const createRecords = async (client, base, recordsToCreate, plan, options, state, recordIdsByKey, app) => {
|
|
6471
6604
|
const mergeRecords = options.autoMerge || (app?.mergeSampleRecords ?? false);
|
|
6472
|
-
const records = [...
|
|
6605
|
+
const records = [...recordsToCreate].sort((a, b) => a.key.localeCompare(b.key, "en"));
|
|
6473
6606
|
for (let offset = 0; offset < records.length; offset += 200) {
|
|
6474
6607
|
const batch = records.slice(offset, offset + 200);
|
|
6475
6608
|
const changeRequest = await client.bases.createBulkChangeRequest({
|
|
6476
6609
|
baseId: base.baseId,
|
|
6477
|
-
records: batch.map((record) => toCreatableFields(base.node, record.fields)),
|
|
6610
|
+
records: batch.map((record) => toCreatableFields(base.node, record.fields, recordIdsByKey)),
|
|
6478
6611
|
message: `Install ${batch.length} record(s) into ${base.node.slug}`,
|
|
6479
6612
|
submittedBy: options.submittedBy,
|
|
6480
6613
|
idempotencyKey: batchIdempotencyKey(plan.tree.manifest.name, batch[0].key),
|
|
@@ -6487,7 +6620,7 @@ const createRecords = async (client, base, plan, options, state, recordIdsByKey,
|
|
|
6487
6620
|
const created = (await approveAndMerge(client, changeRequest.id)).changeRequest.operations.filter((operation) => operation.operation === "record_create").sort((a, b) => a.position - b.position);
|
|
6488
6621
|
for (const [index, record] of batch.entries()) {
|
|
6489
6622
|
const recordId = created[index]?.mergedRecordId;
|
|
6490
|
-
if (recordId) recordIdsByKey.set(record.key, recordId);
|
|
6623
|
+
if (recordId) recordIdsByKey.set(recordIdentity(base.node.slug, record.key), recordId);
|
|
6491
6624
|
}
|
|
6492
6625
|
state.created.records += batch.length;
|
|
6493
6626
|
}
|
|
@@ -6502,30 +6635,31 @@ const createRecords = async (client, base, plan, options, state, recordIdsByKey,
|
|
|
6502
6635
|
* so the same package always produces the same batches and therefore the same keys.
|
|
6503
6636
|
*/
|
|
6504
6637
|
const batchIdempotencyKey = (packageName, firstRecordKey) => `pkg:${packageName}:${firstRecordKey}`;
|
|
6505
|
-
const linkRelations = async (client, base,
|
|
6638
|
+
const linkRelations = async (client, base, relationFields, recordIdsByKey, options, state) => {
|
|
6506
6639
|
for (const record of base.node.records) {
|
|
6507
6640
|
const relationValues = {};
|
|
6508
|
-
let
|
|
6509
|
-
for (const
|
|
6510
|
-
const value = record.fields[slug];
|
|
6511
|
-
const keys =
|
|
6641
|
+
let needsUpdate = false;
|
|
6642
|
+
for (const field of relationFields) {
|
|
6643
|
+
const value = record.fields[field.slug];
|
|
6644
|
+
const keys = toRecordKeyArray(value);
|
|
6512
6645
|
if (keys.length === 0) continue;
|
|
6513
|
-
const
|
|
6514
|
-
const
|
|
6515
|
-
|
|
6646
|
+
const targetBaseSlug = field.options.targetBaseSlug;
|
|
6647
|
+
const targetIds = keys.map((key) => targetBaseSlug ? recordIdsByKey.get(recordIdentity(targetBaseSlug, key)) : void 0).filter((id) => Boolean(id));
|
|
6648
|
+
const unresolved = keys.filter((key) => !targetBaseSlug || !recordIdsByKey.has(recordIdentity(targetBaseSlug, key)));
|
|
6649
|
+
if (unresolved.length > 0) state.warnings.push(`Record "${record.key}" in base "${base.node.slug}" links field "${field.slug}" to ${unresolved.length} record(s) that are not in target Base "${targetBaseSlug || "(unset)"}" — those links were skipped.`);
|
|
6516
6650
|
if (targetIds.length > 0) {
|
|
6517
|
-
relationValues[slug] = targetIds;
|
|
6518
|
-
|
|
6651
|
+
relationValues[field.slug] = targetIds;
|
|
6652
|
+
if (!field.required) needsUpdate = true;
|
|
6519
6653
|
}
|
|
6520
6654
|
}
|
|
6521
|
-
if (!
|
|
6522
|
-
const recordId = recordIdsByKey.get(record.key);
|
|
6655
|
+
if (!needsUpdate) continue;
|
|
6656
|
+
const recordId = recordIdsByKey.get(recordIdentity(base.node.slug, record.key));
|
|
6523
6657
|
if (!recordId) continue;
|
|
6524
6658
|
const changeRequest = await client.records.changeRequest({
|
|
6525
6659
|
operation: "update",
|
|
6526
6660
|
recordId,
|
|
6527
6661
|
fields: {
|
|
6528
|
-
...toCreatableFields(base.node, record.fields),
|
|
6662
|
+
...toCreatableFields(base.node, record.fields, recordIdsByKey),
|
|
6529
6663
|
...relationValues
|
|
6530
6664
|
},
|
|
6531
6665
|
message: `Link relations for ${base.node.slug}`,
|
|
@@ -6536,11 +6670,6 @@ const linkRelations = async (client, base, relationFieldSlugs, recordIdsByKey, o
|
|
|
6536
6670
|
await approveAndMerge(client, changeRequest.id);
|
|
6537
6671
|
}
|
|
6538
6672
|
};
|
|
6539
|
-
const toKeyArray = (value) => {
|
|
6540
|
-
if (Array.isArray(value)) return value.filter((item) => typeof item === "string" && item.length > 0);
|
|
6541
|
-
if (typeof value === "string" && value.length > 0) return [value];
|
|
6542
|
-
return [];
|
|
6543
|
-
};
|
|
6544
6673
|
/** Merging requires an `approved` status, so every auto-merge is review-then-merge. */
|
|
6545
6674
|
const approveAndMerge = async (client, changeRequestId) => {
|
|
6546
6675
|
const reviewResult = (await client.changeRequests.review({
|
|
@@ -7681,6 +7810,7 @@ const buildInstallPlan = (tree, target, options = {}) => {
|
|
|
7681
7810
|
planned = applyRenames(prefixed, collisions, target.existingNodeSlugsByType);
|
|
7682
7811
|
for (const [installedSlug, originalSlug] of collectSlugRewrites(tree, planned)) resourceKeysBySlug[installedSlug] = originalSlug;
|
|
7683
7812
|
}
|
|
7813
|
+
buildRecordCreateLayers(planned.nodes);
|
|
7684
7814
|
collectWarnings(planned, warnings);
|
|
7685
7815
|
const counts = countTree(planned);
|
|
7686
7816
|
if (counts.files > 5e3) throw new Error(`Package has ${counts.files} files, above the ${PACKAGE_MAX_FILE_COUNT}-file limit. Nothing was installed.`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "busa-cli",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.2",
|
|
4
4
|
"description": "Command-line client for the Busabase OpenAPI REST API. Talks to a local or remote `busabase server`. Short-name alias for busabase-cli.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/busabase/busabase/tree/main/apps/busabase-cli",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@zip.js/zip.js": "^2.8.20",
|
|
35
35
|
"busabase-package": "workspace:*",
|
|
36
|
-
"busabase-sdk": "0.19.
|
|
36
|
+
"busabase-sdk": "0.19.2",
|
|
37
37
|
"commander": "^14.0.0",
|
|
38
38
|
"tar-stream": "^3.1.7",
|
|
39
39
|
"yaml": "^2.9.0",
|