@eide/foir-cli 0.4.14 → 0.5.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.
- package/dist/cli.js +164 -106
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -5112,16 +5112,13 @@ async function reconcileApiKeys(client, apiKeys, summary) {
|
|
|
5112
5112
|
}
|
|
5113
5113
|
|
|
5114
5114
|
// src/lib/validate-integrations.ts
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
"managed"
|
|
5123
|
-
]);
|
|
5124
|
-
var KNOWN_EXTENSION_TARGETS = /* @__PURE__ */ new Set(["record", "model-list"]);
|
|
5115
|
+
import { fromJson as fromJson3 } from "@bufbuild/protobuf";
|
|
5116
|
+
import { pathToString } from "@bufbuild/protobuf/reflect";
|
|
5117
|
+
import { createValidator } from "@bufbuild/protovalidate";
|
|
5118
|
+
import {
|
|
5119
|
+
IntegrationConfigSchema,
|
|
5120
|
+
ExtensionConfigSchema
|
|
5121
|
+
} from "@eide/foir-proto-ts/integrations/v1/integrations_pb";
|
|
5125
5122
|
var IntegrationValidationError = class extends Error {
|
|
5126
5123
|
errors;
|
|
5127
5124
|
constructor(errors) {
|
|
@@ -5133,6 +5130,7 @@ ${errors.map((e) => ` - ${e}`).join("\n")}`
|
|
|
5133
5130
|
this.errors = errors;
|
|
5134
5131
|
}
|
|
5135
5132
|
};
|
|
5133
|
+
var validator = createValidator();
|
|
5136
5134
|
function validateIntegrationsAndExtensions(manifest) {
|
|
5137
5135
|
const errors = [];
|
|
5138
5136
|
const warnings = [];
|
|
@@ -5159,29 +5157,14 @@ function assertValid(result) {
|
|
|
5159
5157
|
}
|
|
5160
5158
|
function validateIntegration(name, integration, modelsByKey, errors, warnings) {
|
|
5161
5159
|
const prefix = `integration '${name}'`;
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
errors.push(
|
|
5170
|
-
`${prefix}: middleware.url must be a well-formed https URL, got '${middlewareUrl}'`
|
|
5171
|
-
);
|
|
5172
|
-
}
|
|
5173
|
-
const strategy = integration.credentials?.strategy;
|
|
5174
|
-
if (!strategy) {
|
|
5175
|
-
errors.push(`${prefix}: credentials.strategy is required`);
|
|
5176
|
-
} else if (!KNOWN_CREDENTIAL_STRATEGIES.has(strategy)) {
|
|
5177
|
-
errors.push(
|
|
5178
|
-
`${prefix}: credentials.strategy '${strategy}' is not one of ${[...KNOWN_CREDENTIAL_STRATEGIES].join(", ")}`
|
|
5179
|
-
);
|
|
5180
|
-
}
|
|
5160
|
+
const protoJson = toIntegrationProtoJson(name, integration);
|
|
5161
|
+
const structuralErrors = runProtoValidation(
|
|
5162
|
+
prefix,
|
|
5163
|
+
protoJson,
|
|
5164
|
+
(json) => fromJson3(IntegrationConfigSchema, json, { ignoreUnknownFields: true })
|
|
5165
|
+
);
|
|
5166
|
+
errors.push(...structuralErrors);
|
|
5181
5167
|
const sync = integration.sync ?? {};
|
|
5182
|
-
if (Object.keys(sync).length === 0) {
|
|
5183
|
-
errors.push(`${prefix}: sync must declare at least one source type`);
|
|
5184
|
-
}
|
|
5185
5168
|
for (const [sourceType, mapping] of Object.entries(sync)) {
|
|
5186
5169
|
validateSyncMapping(
|
|
5187
5170
|
`${prefix} sync.${sourceType}`,
|
|
@@ -5194,12 +5177,11 @@ function validateIntegration(name, integration, modelsByKey, errors, warnings) {
|
|
|
5194
5177
|
}
|
|
5195
5178
|
}
|
|
5196
5179
|
function validateSyncMapping(prefix, mapping, integrationName, modelsByKey, errors, warnings) {
|
|
5197
|
-
|
|
5198
|
-
|
|
5180
|
+
const model = mapping.model ? modelsByKey.get(mapping.model) : void 0;
|
|
5181
|
+
const seed = mapping.modelSeed;
|
|
5182
|
+
if (!mapping.model) {
|
|
5199
5183
|
return;
|
|
5200
5184
|
}
|
|
5201
|
-
const model = modelsByKey.get(mapping.model);
|
|
5202
|
-
const seed = mapping.modelSeed;
|
|
5203
5185
|
if (!model && !seed) {
|
|
5204
5186
|
errors.push(
|
|
5205
5187
|
`${prefix}: missing model '${mapping.model}' referenced by integration '${integrationName}' \u2014 declare it under models or provide a modelSeed`
|
|
@@ -5207,52 +5189,22 @@ function validateSyncMapping(prefix, mapping, integrationName, modelsByKey, erro
|
|
|
5207
5189
|
return;
|
|
5208
5190
|
}
|
|
5209
5191
|
const fieldKeys = model ? new Set((model.fields ?? []).map((f) => f.key)) : new Set(Object.keys(seed?.fields ?? {}));
|
|
5210
|
-
|
|
5211
|
-
if (!naturalKeyField || typeof naturalKeyField !== "string") {
|
|
5212
|
-
errors.push(`${prefix}: naturalKey is required`);
|
|
5213
|
-
} else if (!fieldKeys.has(naturalKeyField)) {
|
|
5192
|
+
if (mapping.naturalKey && !fieldKeys.has(mapping.naturalKey)) {
|
|
5214
5193
|
errors.push(
|
|
5215
|
-
`${prefix}: naturalKey '${
|
|
5194
|
+
`${prefix}: naturalKey '${mapping.naturalKey}' does not exist on ${model ? `model '${mapping.model}'` : `modelSeed for '${mapping.model}'`}`
|
|
5216
5195
|
);
|
|
5217
5196
|
}
|
|
5218
5197
|
const fields = mapping.fields ?? {};
|
|
5219
|
-
if (Object.keys(fields).length === 0) {
|
|
5220
|
-
errors.push(`${prefix}: fields mapping must declare at least one field`);
|
|
5221
|
-
}
|
|
5222
5198
|
for (const [sourceField, foirField] of Object.entries(fields)) {
|
|
5223
|
-
if (!foirField
|
|
5224
|
-
errors.push(
|
|
5225
|
-
`${prefix}: field mapping for source '${sourceField}' must be a non-empty string`
|
|
5226
|
-
);
|
|
5227
|
-
continue;
|
|
5228
|
-
}
|
|
5199
|
+
if (!foirField) continue;
|
|
5229
5200
|
if (!fieldKeys.has(foirField)) {
|
|
5230
5201
|
errors.push(
|
|
5231
5202
|
`${prefix}: field mapping '${sourceField} -> ${foirField}' references field '${foirField}' which does not exist on ${model ? `model '${mapping.model}'` : `modelSeed for '${mapping.model}'`}`
|
|
5232
5203
|
);
|
|
5233
5204
|
}
|
|
5234
5205
|
}
|
|
5235
|
-
if (seed) {
|
|
5236
|
-
|
|
5237
|
-
if (model) {
|
|
5238
|
-
detectSeedDrift(prefix, seed, model, warnings);
|
|
5239
|
-
}
|
|
5240
|
-
}
|
|
5241
|
-
}
|
|
5242
|
-
function validateSeedStructure(prefix, seed, errors) {
|
|
5243
|
-
const fields = seed.fields ?? {};
|
|
5244
|
-
if (Object.keys(fields).length === 0) {
|
|
5245
|
-
errors.push(`${prefix}: modelSeed.fields must declare at least one field`);
|
|
5246
|
-
return;
|
|
5247
|
-
}
|
|
5248
|
-
for (const [key, field] of Object.entries(fields)) {
|
|
5249
|
-
if (!field || typeof field !== "object") {
|
|
5250
|
-
errors.push(`${prefix}: modelSeed.fields.${key} must be an object`);
|
|
5251
|
-
continue;
|
|
5252
|
-
}
|
|
5253
|
-
if (!field.type || typeof field.type !== "string") {
|
|
5254
|
-
errors.push(`${prefix}: modelSeed.fields.${key}.type is required`);
|
|
5255
|
-
}
|
|
5206
|
+
if (seed && model) {
|
|
5207
|
+
detectSeedDrift(prefix, seed, model, warnings);
|
|
5256
5208
|
}
|
|
5257
5209
|
}
|
|
5258
5210
|
function detectSeedDrift(prefix, seed, model, warnings) {
|
|
@@ -5275,50 +5227,156 @@ function detectSeedDrift(prefix, seed, model, warnings) {
|
|
|
5275
5227
|
}
|
|
5276
5228
|
function validateExtension(name, extension, modelsByKey, errors) {
|
|
5277
5229
|
const prefix = `extension '${name}'`;
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
`${prefix}: url must be a well-formed https URL, got '${extension.url}'`
|
|
5286
|
-
);
|
|
5287
|
-
}
|
|
5230
|
+
const protoJson = toExtensionProtoJson(name, extension);
|
|
5231
|
+
const structuralErrors = runProtoValidation(
|
|
5232
|
+
prefix,
|
|
5233
|
+
protoJson,
|
|
5234
|
+
(json) => fromJson3(ExtensionConfigSchema, json, { ignoreUnknownFields: true })
|
|
5235
|
+
);
|
|
5236
|
+
errors.push(...structuralErrors);
|
|
5288
5237
|
const placements = extension.placements ?? [];
|
|
5289
|
-
if (placements.length === 0) {
|
|
5290
|
-
errors.push(`${prefix}: placements must declare at least one placement`);
|
|
5291
|
-
}
|
|
5292
5238
|
placements.forEach((placement, index) => {
|
|
5293
|
-
|
|
5294
|
-
if (!placement.target || !KNOWN_EXTENSION_TARGETS.has(placement.target)) {
|
|
5239
|
+
if (placement.model && !modelsByKey.has(placement.model)) {
|
|
5295
5240
|
errors.push(
|
|
5296
|
-
`${
|
|
5241
|
+
`${prefix} placements[${index}]: missing model '${placement.model}' referenced by extension '${name}'`
|
|
5297
5242
|
);
|
|
5298
5243
|
}
|
|
5299
|
-
if (!placement.model || typeof placement.model !== "string") {
|
|
5300
|
-
errors.push(`${placementPrefix}: model is required`);
|
|
5301
|
-
} else if (!modelsByKey.has(placement.model)) {
|
|
5302
|
-
errors.push(
|
|
5303
|
-
`${placementPrefix}: missing model '${placement.model}' referenced by extension '${name}'`
|
|
5304
|
-
);
|
|
5305
|
-
}
|
|
5306
|
-
if (!placement.tab || typeof placement.tab !== "string") {
|
|
5307
|
-
errors.push(`${placementPrefix}: tab is required`);
|
|
5308
|
-
}
|
|
5309
|
-
if (!placement.title || typeof placement.title !== "string") {
|
|
5310
|
-
errors.push(`${placementPrefix}: title is required`);
|
|
5311
|
-
}
|
|
5312
5244
|
});
|
|
5313
5245
|
}
|
|
5314
|
-
function
|
|
5246
|
+
function runProtoValidation(prefix, json, decode) {
|
|
5247
|
+
let message;
|
|
5248
|
+
try {
|
|
5249
|
+
message = decode(json);
|
|
5250
|
+
} catch (err) {
|
|
5251
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5252
|
+
return [`${prefix}: cannot decode as proto \u2014 ${msg}`];
|
|
5253
|
+
}
|
|
5254
|
+
const result = runValidate(message);
|
|
5255
|
+
if (result.kind === "valid") return [];
|
|
5256
|
+
if (result.kind === "error") {
|
|
5257
|
+
return [`${prefix}: validator error \u2014 ${result.error.message}`];
|
|
5258
|
+
}
|
|
5259
|
+
return result.violations.map((v) => `${prefix}: ${formatViolation(v)}`);
|
|
5260
|
+
}
|
|
5261
|
+
function runValidate(message) {
|
|
5262
|
+
if (!message || typeof message !== "object") {
|
|
5263
|
+
return { kind: "error", error: new Error("message is not an object") };
|
|
5264
|
+
}
|
|
5265
|
+
const anyMsg = message;
|
|
5266
|
+
let schema;
|
|
5267
|
+
if (anyMsg.$typeName === "integrations.v1.IntegrationConfig") {
|
|
5268
|
+
schema = IntegrationConfigSchema;
|
|
5269
|
+
} else if (anyMsg.$typeName === "integrations.v1.ExtensionConfig") {
|
|
5270
|
+
schema = ExtensionConfigSchema;
|
|
5271
|
+
} else {
|
|
5272
|
+
return { kind: "error", error: new Error(`unknown message type ${anyMsg.$typeName ?? "?"}`) };
|
|
5273
|
+
}
|
|
5274
|
+
const result = validator.validate(
|
|
5275
|
+
// Both schema/message casts are safe because we picked schema by $typeName.
|
|
5276
|
+
schema,
|
|
5277
|
+
message
|
|
5278
|
+
);
|
|
5279
|
+
if (result.kind === "valid") return { kind: "valid" };
|
|
5280
|
+
if (result.kind === "error") return { kind: "error", error: result.error };
|
|
5281
|
+
return { kind: "invalid", violations: result.violations };
|
|
5282
|
+
}
|
|
5283
|
+
function formatViolation(v) {
|
|
5284
|
+
let path3 = "";
|
|
5315
5285
|
try {
|
|
5316
|
-
|
|
5317
|
-
if (parsed.protocol !== "https:") return false;
|
|
5318
|
-
if (!parsed.hostname) return false;
|
|
5319
|
-
return true;
|
|
5286
|
+
path3 = pathToString(v.field);
|
|
5320
5287
|
} catch {
|
|
5321
|
-
|
|
5288
|
+
path3 = "";
|
|
5289
|
+
}
|
|
5290
|
+
const message = v.message ?? "";
|
|
5291
|
+
return path3 ? `${path3}: ${message}` : message;
|
|
5292
|
+
}
|
|
5293
|
+
function compact(obj) {
|
|
5294
|
+
const out = {};
|
|
5295
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
5296
|
+
if (v !== void 0) out[k] = v;
|
|
5297
|
+
}
|
|
5298
|
+
return out;
|
|
5299
|
+
}
|
|
5300
|
+
function toIntegrationProtoJson(name, integration) {
|
|
5301
|
+
return compact({
|
|
5302
|
+
name,
|
|
5303
|
+
enabled: integration.enabled ?? true,
|
|
5304
|
+
middleware: integration.middleware ? { url: integration.middleware.url ?? "" } : void 0,
|
|
5305
|
+
credentials: {
|
|
5306
|
+
strategy: integration.credentials?.strategy ? credentialStrategyToProto(integration.credentials.strategy) : "CREDENTIAL_STRATEGY_UNSPECIFIED"
|
|
5307
|
+
},
|
|
5308
|
+
sync: integration.sync ? Object.fromEntries(
|
|
5309
|
+
Object.entries(integration.sync).map(([k, v]) => [k, syncMappingToProto(v)])
|
|
5310
|
+
) : {},
|
|
5311
|
+
settings: integration.settings,
|
|
5312
|
+
metadata: integration.metadata
|
|
5313
|
+
});
|
|
5314
|
+
}
|
|
5315
|
+
function syncMappingToProto(mapping) {
|
|
5316
|
+
return compact({
|
|
5317
|
+
model: mapping.model ?? "",
|
|
5318
|
+
naturalKey: mapping.naturalKey ?? "",
|
|
5319
|
+
fields: mapping.fields ?? {},
|
|
5320
|
+
modelSeed: mapping.modelSeed ? {
|
|
5321
|
+
fields: Object.fromEntries(
|
|
5322
|
+
Object.entries(mapping.modelSeed.fields ?? {}).map(([k, v]) => [
|
|
5323
|
+
k,
|
|
5324
|
+
compact({
|
|
5325
|
+
type: v.type ?? "",
|
|
5326
|
+
required: v.required ?? false,
|
|
5327
|
+
naturalKey: v.naturalKey ?? false,
|
|
5328
|
+
label: v.label,
|
|
5329
|
+
helpText: v.helpText,
|
|
5330
|
+
config: v.config
|
|
5331
|
+
})
|
|
5332
|
+
])
|
|
5333
|
+
)
|
|
5334
|
+
} : void 0
|
|
5335
|
+
});
|
|
5336
|
+
}
|
|
5337
|
+
function credentialStrategyToProto(strategy) {
|
|
5338
|
+
switch (strategy) {
|
|
5339
|
+
case "oauth":
|
|
5340
|
+
return "CREDENTIAL_STRATEGY_OAUTH";
|
|
5341
|
+
case "api_key":
|
|
5342
|
+
return "CREDENTIAL_STRATEGY_API_KEY";
|
|
5343
|
+
case "shared_secret":
|
|
5344
|
+
return "CREDENTIAL_STRATEGY_SHARED_SECRET";
|
|
5345
|
+
case "ssh_key":
|
|
5346
|
+
return "CREDENTIAL_STRATEGY_SSH_KEY";
|
|
5347
|
+
case "none":
|
|
5348
|
+
return "CREDENTIAL_STRATEGY_NONE";
|
|
5349
|
+
case "managed":
|
|
5350
|
+
return "CREDENTIAL_STRATEGY_MANAGED";
|
|
5351
|
+
default:
|
|
5352
|
+
return "CREDENTIAL_STRATEGY_UNSPECIFIED";
|
|
5353
|
+
}
|
|
5354
|
+
}
|
|
5355
|
+
function toExtensionProtoJson(name, extension) {
|
|
5356
|
+
const placements = (extension.placements ?? []).map(
|
|
5357
|
+
(p) => compact({
|
|
5358
|
+
target: extensionTargetToProto(p.target),
|
|
5359
|
+
model: p.model ?? "",
|
|
5360
|
+
tab: p.tab ?? "",
|
|
5361
|
+
title: p.title ?? "",
|
|
5362
|
+
hints: p.hints
|
|
5363
|
+
})
|
|
5364
|
+
);
|
|
5365
|
+
return compact({
|
|
5366
|
+
name,
|
|
5367
|
+
url: extension.url ?? "",
|
|
5368
|
+
placements,
|
|
5369
|
+
metadata: extension.metadata
|
|
5370
|
+
});
|
|
5371
|
+
}
|
|
5372
|
+
function extensionTargetToProto(target) {
|
|
5373
|
+
switch (target) {
|
|
5374
|
+
case "record":
|
|
5375
|
+
return "EXTENSION_TARGET_RECORD";
|
|
5376
|
+
case "model-list":
|
|
5377
|
+
return "EXTENSION_TARGET_MODEL_LIST";
|
|
5378
|
+
default:
|
|
5379
|
+
return "EXTENSION_TARGET_UNSPECIFIED";
|
|
5322
5380
|
}
|
|
5323
5381
|
}
|
|
5324
5382
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eide/foir-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Universal platform CLI for Foir platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -47,9 +47,10 @@
|
|
|
47
47
|
"license": "UNLICENSED",
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@bufbuild/protobuf": "^2.0.0",
|
|
50
|
+
"@bufbuild/protovalidate": "^1.1.1",
|
|
50
51
|
"@connectrpc/connect": "^2.0.0",
|
|
51
52
|
"@connectrpc/connect-node": "^2.0.0",
|
|
52
|
-
"@eide/foir-proto-ts": "^0.
|
|
53
|
+
"@eide/foir-proto-ts": "^0.10.0",
|
|
53
54
|
"chalk": "^5.3.0",
|
|
54
55
|
"commander": "^12.1.0",
|
|
55
56
|
"dotenv": "^16.4.5",
|