@carthooks/arcubase-cli 0.1.12 → 0.1.13
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/bundle/arcubase-admin.mjs +95 -4
- package/bundle/arcubase.mjs +95 -4
- package/dist/runtime/errors.d.ts +1 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/execute.d.ts.map +1 -1
- package/dist/runtime/execute.js +105 -3
- package/dist/tests/execute_validation.test.js +62 -0
- package/dist/tests/help.test.js +2 -0
- package/package.json +1 -1
- package/sdk-dist/docs/runtime-reference/access-rule.md +19 -0
- package/sdk-dist/docs/runtime-reference/row-crud.md +13 -0
- package/src/runtime/errors.ts +1 -0
- package/src/runtime/execute.ts +110 -4
- package/src/tests/execute_validation.test.ts +69 -0
- package/src/tests/help.test.ts +2 -0
|
@@ -6660,8 +6660,9 @@ function renderCommandHelp(scope, moduleName, commandName, env = process.env) {
|
|
|
6660
6660
|
' - update policy: {"update":["options"],"options":{"policy":{"key":"custom","actions":["view","add","edit"],"all_fields_read":true,"all_fields_write":true,"fields":[{"key":":1002","read":false,"write":false,"report":false}]},"list_options":{}}}'
|
|
6661
6661
|
] : [],
|
|
6662
6662
|
...scope === "admin" && command.commandPath[0] === "access-rule" && command.commandPath[1] === "create" ? [
|
|
6663
|
-
"body-json
|
|
6664
|
-
|
|
6663
|
+
"body-json examples:",
|
|
6664
|
+
` - full access: ${JSON.stringify(buildFullAccessRuleBody())}`,
|
|
6665
|
+
` - hide field: ${JSON.stringify(buildHiddenFieldAccessRuleBody())}`,
|
|
6665
6666
|
"success requires:",
|
|
6666
6667
|
" - result.enabled must be true",
|
|
6667
6668
|
' - options.user_scope[].type is "user" for tenant users',
|
|
@@ -7040,6 +7041,26 @@ function isAccessRuleUpdateCommand(scope, command) {
|
|
|
7040
7041
|
return scope === "admin" && command.commandPath[0] === "access-rule" && command.commandPath[1] === "update";
|
|
7041
7042
|
}
|
|
7042
7043
|
function buildAccessRuleShapeHint(userId = 2188889901) {
|
|
7044
|
+
return buildHiddenFieldAccessRuleBody(userId);
|
|
7045
|
+
}
|
|
7046
|
+
function buildFullAccessRuleBody(userId = 2188889977) {
|
|
7047
|
+
return {
|
|
7048
|
+
name: "Admin full access",
|
|
7049
|
+
enabled: true,
|
|
7050
|
+
type: "form",
|
|
7051
|
+
options: {
|
|
7052
|
+
user_scope: [{ type: "user", id: userId }],
|
|
7053
|
+
policy: {
|
|
7054
|
+
key: "custom",
|
|
7055
|
+
actions: ["view", "add", "edit", "delete"],
|
|
7056
|
+
all_fields_read: true,
|
|
7057
|
+
all_fields_write: true
|
|
7058
|
+
},
|
|
7059
|
+
list_options: {}
|
|
7060
|
+
}
|
|
7061
|
+
};
|
|
7062
|
+
}
|
|
7063
|
+
function buildHiddenFieldAccessRuleBody(userId = 2188889901) {
|
|
7043
7064
|
return {
|
|
7044
7065
|
name: "Sales read write",
|
|
7045
7066
|
enabled: true,
|
|
@@ -7057,6 +7078,12 @@ function buildAccessRuleShapeHint(userId = 2188889901) {
|
|
|
7057
7078
|
}
|
|
7058
7079
|
};
|
|
7059
7080
|
}
|
|
7081
|
+
function buildAccessRuleCreateToolCall(body) {
|
|
7082
|
+
return {
|
|
7083
|
+
command: "access-rule create",
|
|
7084
|
+
args: ["--app-id", "<app_id>", "--table-id", "<table_id>", "--body-json", JSON.stringify(body)]
|
|
7085
|
+
};
|
|
7086
|
+
}
|
|
7060
7087
|
function buildAssignUsersValidationDetails(scope, command, requestType, path, message, env = process.env) {
|
|
7061
7088
|
return {
|
|
7062
7089
|
operation: `${scope}.${command.module}.${command.functionName}`,
|
|
@@ -7194,7 +7221,15 @@ function buildBodyGuidance(requestType) {
|
|
|
7194
7221
|
};
|
|
7195
7222
|
}
|
|
7196
7223
|
if (requestType === "AppIngressCreateReqVO" || requestType === "AppIngressUpdateReqVO") {
|
|
7224
|
+
const retryToolCalls = [
|
|
7225
|
+
buildAccessRuleCreateToolCall(buildFullAccessRuleBody()),
|
|
7226
|
+
buildAccessRuleCreateToolCall(buildHiddenFieldAccessRuleBody())
|
|
7227
|
+
];
|
|
7197
7228
|
return {
|
|
7229
|
+
retryToolCalls,
|
|
7230
|
+
suggestions: [
|
|
7231
|
+
"next arcubase-admin-cli input: retryToolCalls[0] for full access or retryToolCalls[1] for hidden-field access"
|
|
7232
|
+
],
|
|
7198
7233
|
shapeHint: buildAccessRuleShapeHint(),
|
|
7199
7234
|
commonMistakes: [
|
|
7200
7235
|
"use options.policy, not top-level permissions",
|
|
@@ -7647,6 +7682,60 @@ function unwrapData(value) {
|
|
|
7647
7682
|
}
|
|
7648
7683
|
return current;
|
|
7649
7684
|
}
|
|
7685
|
+
function numericFieldKeys(fields) {
|
|
7686
|
+
if (!isRecord2(fields)) return [];
|
|
7687
|
+
return Object.keys(fields).filter((key) => /^\d+$/.test(key)).sort((left, right) => Number(left) - Number(right));
|
|
7688
|
+
}
|
|
7689
|
+
function permitFieldId(permit) {
|
|
7690
|
+
if (!isRecord2(permit) || typeof permit.key !== "string") return void 0;
|
|
7691
|
+
const match = /^:(\d+)$/.exec(permit.key);
|
|
7692
|
+
return match?.[1];
|
|
7693
|
+
}
|
|
7694
|
+
function rowFieldPermits(value) {
|
|
7695
|
+
const data = unwrapData(value);
|
|
7696
|
+
const fieldPermits = isRecord2(data) && isRecord2(data.fieldPermits) ? data.fieldPermits : void 0;
|
|
7697
|
+
return fieldPermits && Array.isArray(fieldPermits.fields) ? fieldPermits.fields.filter((item) => isRecord2(item)) : [];
|
|
7698
|
+
}
|
|
7699
|
+
function buildRowValueEvidence(command, responseBody) {
|
|
7700
|
+
if (command.scope !== "user" || command.commandPath[0] !== "row") return void 0;
|
|
7701
|
+
const action = command.commandPath[1];
|
|
7702
|
+
const data = unwrapData(responseBody);
|
|
7703
|
+
const rules = [
|
|
7704
|
+
"current row values come only from row query fields or row get record.fields",
|
|
7705
|
+
"table schema fields[].value and default_value_mode are schema defaults, not returned row values",
|
|
7706
|
+
"a field id absent from returned row fields has no row-value evidence",
|
|
7707
|
+
"read visibility status is fieldPermits read:false; FIELD_PERMISSION_REQUIRED is only a write preflight error"
|
|
7708
|
+
];
|
|
7709
|
+
if (action === "get") {
|
|
7710
|
+
const record = isRecord2(data) && isRecord2(data.record) ? data.record : void 0;
|
|
7711
|
+
const returnedFieldIds = numericFieldKeys(record?.fields);
|
|
7712
|
+
const permits = rowFieldPermits(responseBody);
|
|
7713
|
+
const permitFieldIds = permits.map(permitFieldId).filter((id) => Boolean(id));
|
|
7714
|
+
const deniedReadFieldIds = permits.filter((permit) => permit.read === false).map(permitFieldId).filter((id) => Boolean(id)).sort((left, right) => Number(left) - Number(right));
|
|
7715
|
+
const notReturnedFieldIds = permitFieldIds.filter((id) => !returnedFieldIds.includes(id)).sort((left, right) => Number(left) - Number(right));
|
|
7716
|
+
return {
|
|
7717
|
+
source: "row get record.fields",
|
|
7718
|
+
returnedFieldIds,
|
|
7719
|
+
notReturnedFieldIds,
|
|
7720
|
+
deniedReadFieldIds,
|
|
7721
|
+
absentFieldMeaning: "not returned to current user; no row-value evidence",
|
|
7722
|
+
rules
|
|
7723
|
+
};
|
|
7724
|
+
}
|
|
7725
|
+
if (action === "query") {
|
|
7726
|
+
const items = isRecord2(data) && Array.isArray(data.items) ? data.items : [];
|
|
7727
|
+
return {
|
|
7728
|
+
source: "row query items[].fields",
|
|
7729
|
+
rows: items.filter((item) => isRecord2(item)).map((item) => ({
|
|
7730
|
+
rowId: item.id,
|
|
7731
|
+
returnedFieldIds: numericFieldKeys(item.fields)
|
|
7732
|
+
})),
|
|
7733
|
+
absentFieldMeaning: "not returned to current user; no row-value evidence",
|
|
7734
|
+
rules
|
|
7735
|
+
};
|
|
7736
|
+
}
|
|
7737
|
+
return void 0;
|
|
7738
|
+
}
|
|
7650
7739
|
function requiredRowAction(command, flags) {
|
|
7651
7740
|
if (command.commandPath[0] !== "row") return void 0;
|
|
7652
7741
|
switch (command.commandPath[1]) {
|
|
@@ -7954,7 +8043,7 @@ async function executeCLI(scope, argv, env, fetchImpl = fetch) {
|
|
|
7954
8043
|
if (error instanceof CLIError && error.code === "INVALID_BODY_JSON" && command.requestType) {
|
|
7955
8044
|
throw new CLIError(
|
|
7956
8045
|
"INVALID_BODY_JSON",
|
|
7957
|
-
`body json is invalid for ${scope}.${command.commandPath.join(" ")};
|
|
8046
|
+
`body json is invalid for ${scope}.${command.commandPath.join(" ")}; retry with retryToolCalls[0] or retryToolCalls[1] when present`,
|
|
7958
8047
|
2,
|
|
7959
8048
|
buildBodyValidationDetails(scope, command, command.requestType, [{ path: "body", message: "invalid JSON syntax" }], runtimeEnv)
|
|
7960
8049
|
);
|
|
@@ -8091,11 +8180,13 @@ async function executeCLI(scope, argv, env, fetchImpl = fetch) {
|
|
|
8091
8180
|
} : {}
|
|
8092
8181
|
});
|
|
8093
8182
|
}
|
|
8183
|
+
const rowValueEvidence = buildRowValueEvidence(command, parsedResponse);
|
|
8094
8184
|
return {
|
|
8095
8185
|
kind: "result",
|
|
8096
8186
|
commandPath: command.commandPath,
|
|
8097
8187
|
status: response.status,
|
|
8098
|
-
data: parsedResponse
|
|
8188
|
+
data: parsedResponse,
|
|
8189
|
+
...rowValueEvidence ? { rowValueEvidence } : {}
|
|
8099
8190
|
};
|
|
8100
8191
|
}
|
|
8101
8192
|
|
package/bundle/arcubase.mjs
CHANGED
|
@@ -6660,8 +6660,9 @@ function renderCommandHelp(scope, moduleName, commandName, env = process.env) {
|
|
|
6660
6660
|
' - update policy: {"update":["options"],"options":{"policy":{"key":"custom","actions":["view","add","edit"],"all_fields_read":true,"all_fields_write":true,"fields":[{"key":":1002","read":false,"write":false,"report":false}]},"list_options":{}}}'
|
|
6661
6661
|
] : [],
|
|
6662
6662
|
...scope === "admin" && command.commandPath[0] === "access-rule" && command.commandPath[1] === "create" ? [
|
|
6663
|
-
"body-json
|
|
6664
|
-
|
|
6663
|
+
"body-json examples:",
|
|
6664
|
+
` - full access: ${JSON.stringify(buildFullAccessRuleBody())}`,
|
|
6665
|
+
` - hide field: ${JSON.stringify(buildHiddenFieldAccessRuleBody())}`,
|
|
6665
6666
|
"success requires:",
|
|
6666
6667
|
" - result.enabled must be true",
|
|
6667
6668
|
' - options.user_scope[].type is "user" for tenant users',
|
|
@@ -7040,6 +7041,26 @@ function isAccessRuleUpdateCommand(scope, command) {
|
|
|
7040
7041
|
return scope === "admin" && command.commandPath[0] === "access-rule" && command.commandPath[1] === "update";
|
|
7041
7042
|
}
|
|
7042
7043
|
function buildAccessRuleShapeHint(userId = 2188889901) {
|
|
7044
|
+
return buildHiddenFieldAccessRuleBody(userId);
|
|
7045
|
+
}
|
|
7046
|
+
function buildFullAccessRuleBody(userId = 2188889977) {
|
|
7047
|
+
return {
|
|
7048
|
+
name: "Admin full access",
|
|
7049
|
+
enabled: true,
|
|
7050
|
+
type: "form",
|
|
7051
|
+
options: {
|
|
7052
|
+
user_scope: [{ type: "user", id: userId }],
|
|
7053
|
+
policy: {
|
|
7054
|
+
key: "custom",
|
|
7055
|
+
actions: ["view", "add", "edit", "delete"],
|
|
7056
|
+
all_fields_read: true,
|
|
7057
|
+
all_fields_write: true
|
|
7058
|
+
},
|
|
7059
|
+
list_options: {}
|
|
7060
|
+
}
|
|
7061
|
+
};
|
|
7062
|
+
}
|
|
7063
|
+
function buildHiddenFieldAccessRuleBody(userId = 2188889901) {
|
|
7043
7064
|
return {
|
|
7044
7065
|
name: "Sales read write",
|
|
7045
7066
|
enabled: true,
|
|
@@ -7057,6 +7078,12 @@ function buildAccessRuleShapeHint(userId = 2188889901) {
|
|
|
7057
7078
|
}
|
|
7058
7079
|
};
|
|
7059
7080
|
}
|
|
7081
|
+
function buildAccessRuleCreateToolCall(body) {
|
|
7082
|
+
return {
|
|
7083
|
+
command: "access-rule create",
|
|
7084
|
+
args: ["--app-id", "<app_id>", "--table-id", "<table_id>", "--body-json", JSON.stringify(body)]
|
|
7085
|
+
};
|
|
7086
|
+
}
|
|
7060
7087
|
function buildAssignUsersValidationDetails(scope, command, requestType, path, message, env = process.env) {
|
|
7061
7088
|
return {
|
|
7062
7089
|
operation: `${scope}.${command.module}.${command.functionName}`,
|
|
@@ -7194,7 +7221,15 @@ function buildBodyGuidance(requestType) {
|
|
|
7194
7221
|
};
|
|
7195
7222
|
}
|
|
7196
7223
|
if (requestType === "AppIngressCreateReqVO" || requestType === "AppIngressUpdateReqVO") {
|
|
7224
|
+
const retryToolCalls = [
|
|
7225
|
+
buildAccessRuleCreateToolCall(buildFullAccessRuleBody()),
|
|
7226
|
+
buildAccessRuleCreateToolCall(buildHiddenFieldAccessRuleBody())
|
|
7227
|
+
];
|
|
7197
7228
|
return {
|
|
7229
|
+
retryToolCalls,
|
|
7230
|
+
suggestions: [
|
|
7231
|
+
"next arcubase-admin-cli input: retryToolCalls[0] for full access or retryToolCalls[1] for hidden-field access"
|
|
7232
|
+
],
|
|
7198
7233
|
shapeHint: buildAccessRuleShapeHint(),
|
|
7199
7234
|
commonMistakes: [
|
|
7200
7235
|
"use options.policy, not top-level permissions",
|
|
@@ -7647,6 +7682,60 @@ function unwrapData(value) {
|
|
|
7647
7682
|
}
|
|
7648
7683
|
return current;
|
|
7649
7684
|
}
|
|
7685
|
+
function numericFieldKeys(fields) {
|
|
7686
|
+
if (!isRecord2(fields)) return [];
|
|
7687
|
+
return Object.keys(fields).filter((key) => /^\d+$/.test(key)).sort((left, right) => Number(left) - Number(right));
|
|
7688
|
+
}
|
|
7689
|
+
function permitFieldId(permit) {
|
|
7690
|
+
if (!isRecord2(permit) || typeof permit.key !== "string") return void 0;
|
|
7691
|
+
const match = /^:(\d+)$/.exec(permit.key);
|
|
7692
|
+
return match?.[1];
|
|
7693
|
+
}
|
|
7694
|
+
function rowFieldPermits(value) {
|
|
7695
|
+
const data = unwrapData(value);
|
|
7696
|
+
const fieldPermits = isRecord2(data) && isRecord2(data.fieldPermits) ? data.fieldPermits : void 0;
|
|
7697
|
+
return fieldPermits && Array.isArray(fieldPermits.fields) ? fieldPermits.fields.filter((item) => isRecord2(item)) : [];
|
|
7698
|
+
}
|
|
7699
|
+
function buildRowValueEvidence(command, responseBody) {
|
|
7700
|
+
if (command.scope !== "user" || command.commandPath[0] !== "row") return void 0;
|
|
7701
|
+
const action = command.commandPath[1];
|
|
7702
|
+
const data = unwrapData(responseBody);
|
|
7703
|
+
const rules = [
|
|
7704
|
+
"current row values come only from row query fields or row get record.fields",
|
|
7705
|
+
"table schema fields[].value and default_value_mode are schema defaults, not returned row values",
|
|
7706
|
+
"a field id absent from returned row fields has no row-value evidence",
|
|
7707
|
+
"read visibility status is fieldPermits read:false; FIELD_PERMISSION_REQUIRED is only a write preflight error"
|
|
7708
|
+
];
|
|
7709
|
+
if (action === "get") {
|
|
7710
|
+
const record = isRecord2(data) && isRecord2(data.record) ? data.record : void 0;
|
|
7711
|
+
const returnedFieldIds = numericFieldKeys(record?.fields);
|
|
7712
|
+
const permits = rowFieldPermits(responseBody);
|
|
7713
|
+
const permitFieldIds = permits.map(permitFieldId).filter((id) => Boolean(id));
|
|
7714
|
+
const deniedReadFieldIds = permits.filter((permit) => permit.read === false).map(permitFieldId).filter((id) => Boolean(id)).sort((left, right) => Number(left) - Number(right));
|
|
7715
|
+
const notReturnedFieldIds = permitFieldIds.filter((id) => !returnedFieldIds.includes(id)).sort((left, right) => Number(left) - Number(right));
|
|
7716
|
+
return {
|
|
7717
|
+
source: "row get record.fields",
|
|
7718
|
+
returnedFieldIds,
|
|
7719
|
+
notReturnedFieldIds,
|
|
7720
|
+
deniedReadFieldIds,
|
|
7721
|
+
absentFieldMeaning: "not returned to current user; no row-value evidence",
|
|
7722
|
+
rules
|
|
7723
|
+
};
|
|
7724
|
+
}
|
|
7725
|
+
if (action === "query") {
|
|
7726
|
+
const items = isRecord2(data) && Array.isArray(data.items) ? data.items : [];
|
|
7727
|
+
return {
|
|
7728
|
+
source: "row query items[].fields",
|
|
7729
|
+
rows: items.filter((item) => isRecord2(item)).map((item) => ({
|
|
7730
|
+
rowId: item.id,
|
|
7731
|
+
returnedFieldIds: numericFieldKeys(item.fields)
|
|
7732
|
+
})),
|
|
7733
|
+
absentFieldMeaning: "not returned to current user; no row-value evidence",
|
|
7734
|
+
rules
|
|
7735
|
+
};
|
|
7736
|
+
}
|
|
7737
|
+
return void 0;
|
|
7738
|
+
}
|
|
7650
7739
|
function requiredRowAction(command, flags) {
|
|
7651
7740
|
if (command.commandPath[0] !== "row") return void 0;
|
|
7652
7741
|
switch (command.commandPath[1]) {
|
|
@@ -7954,7 +8043,7 @@ async function executeCLI(scope, argv, env, fetchImpl = fetch) {
|
|
|
7954
8043
|
if (error instanceof CLIError && error.code === "INVALID_BODY_JSON" && command.requestType) {
|
|
7955
8044
|
throw new CLIError(
|
|
7956
8045
|
"INVALID_BODY_JSON",
|
|
7957
|
-
`body json is invalid for ${scope}.${command.commandPath.join(" ")};
|
|
8046
|
+
`body json is invalid for ${scope}.${command.commandPath.join(" ")}; retry with retryToolCalls[0] or retryToolCalls[1] when present`,
|
|
7958
8047
|
2,
|
|
7959
8048
|
buildBodyValidationDetails(scope, command, command.requestType, [{ path: "body", message: "invalid JSON syntax" }], runtimeEnv)
|
|
7960
8049
|
);
|
|
@@ -8091,11 +8180,13 @@ async function executeCLI(scope, argv, env, fetchImpl = fetch) {
|
|
|
8091
8180
|
} : {}
|
|
8092
8181
|
});
|
|
8093
8182
|
}
|
|
8183
|
+
const rowValueEvidence = buildRowValueEvidence(command, parsedResponse);
|
|
8094
8184
|
return {
|
|
8095
8185
|
kind: "result",
|
|
8096
8186
|
commandPath: command.commandPath,
|
|
8097
8187
|
status: response.status,
|
|
8098
|
-
data: parsedResponse
|
|
8188
|
+
data: parsedResponse,
|
|
8189
|
+
...rowValueEvidence ? { rowValueEvidence } : {}
|
|
8099
8190
|
};
|
|
8100
8191
|
}
|
|
8101
8192
|
|
package/dist/runtime/errors.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/runtime/errors.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,kBAAkB,EAAE,CAAA;IAC7B,OAAO,CAAC,EAAE,aAAa,EAAE,CAAA;IACzB,QAAQ,CAAC,EAAE,UAAU,EAAE,CAAA;IACvB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IACzB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CACvB,CAAA;AAED,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,CAAA;gBAEtB,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,SAAI,EAAE,OAAO,CAAC,EAAE,eAAe;IAQlF,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CASlC"}
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/runtime/errors.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,kBAAkB,EAAE,CAAA;IAC7B,OAAO,CAAC,EAAE,aAAa,EAAE,CAAA;IACzB,QAAQ,CAAC,EAAE,UAAU,EAAE,CAAA;IACvB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IACzB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,cAAc,CAAC,EAAE,OAAO,EAAE,CAAA;IAC1B,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CACvB,CAAA;AAED,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,CAAA;gBAEtB,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,SAAI,EAAE,OAAO,CAAC,EAAE,eAAe;IAQlF,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CASlC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"execute.d.ts","sourceRoot":"","sources":["../../src/runtime/execute.ts"],"names":[],"mappings":"AACA,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,UAAU,CAAA;AAG1D,OAAO,EAAwE,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAK/H,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,EAAE,YAAY,CAAA;IACnB,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;IAC5B,UAAU,EAAE,SAAS,MAAM,EAAE,CAAA;CAC9B,CAAA;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAc1D;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,GAAE,QAAsB,GAAG,MAAM,CAoB7G;AAOD,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;AAMlD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,EAAE,GAAG,GAAE,QAAsB,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"execute.d.ts","sourceRoot":"","sources":["../../src/runtime/execute.ts"],"names":[],"mappings":"AACA,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,UAAU,CAAA;AAG1D,OAAO,EAAwE,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAK/H,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,EAAE,YAAY,CAAA;IACnB,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;IAC5B,UAAU,EAAE,SAAS,MAAM,EAAE,CAAA;CAC9B,CAAA;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAc1D;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,GAAE,QAAsB,GAAG,MAAM,CAoB7G;AAOD,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;AAMlD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,EAAE,GAAG,GAAE,QAAsB,GAAG,MAAM,CAmFpI;AA8/CD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,uBAAuB,CActH;AAED,wBAAsB,UAAU,CAAC,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,CAAC,EAAE,UAAU,EAAE,SAAS,GAAE,OAAO,KAAa,GAAG,OAAO,CAAC,GAAG,CAAC,CAmMrI"}
|
package/dist/runtime/execute.js
CHANGED
|
@@ -104,8 +104,9 @@ export function renderCommandHelp(scope, moduleName, commandName, env = process.
|
|
|
104
104
|
: []),
|
|
105
105
|
...(scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'create'
|
|
106
106
|
? [
|
|
107
|
-
'body-json
|
|
108
|
-
|
|
107
|
+
'body-json examples:',
|
|
108
|
+
` - full access: ${JSON.stringify(buildFullAccessRuleBody())}`,
|
|
109
|
+
` - hide field: ${JSON.stringify(buildHiddenFieldAccessRuleBody())}`,
|
|
109
110
|
'success requires:',
|
|
110
111
|
' - result.enabled must be true',
|
|
111
112
|
' - options.user_scope[].type is "user" for tenant users',
|
|
@@ -510,6 +511,26 @@ function isAccessRuleUpdateCommand(scope, command) {
|
|
|
510
511
|
return scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'update';
|
|
511
512
|
}
|
|
512
513
|
function buildAccessRuleShapeHint(userId = 2188889901) {
|
|
514
|
+
return buildHiddenFieldAccessRuleBody(userId);
|
|
515
|
+
}
|
|
516
|
+
function buildFullAccessRuleBody(userId = 2188889977) {
|
|
517
|
+
return {
|
|
518
|
+
name: 'Admin full access',
|
|
519
|
+
enabled: true,
|
|
520
|
+
type: 'form',
|
|
521
|
+
options: {
|
|
522
|
+
user_scope: [{ type: 'user', id: userId }],
|
|
523
|
+
policy: {
|
|
524
|
+
key: 'custom',
|
|
525
|
+
actions: ['view', 'add', 'edit', 'delete'],
|
|
526
|
+
all_fields_read: true,
|
|
527
|
+
all_fields_write: true,
|
|
528
|
+
},
|
|
529
|
+
list_options: {},
|
|
530
|
+
},
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
function buildHiddenFieldAccessRuleBody(userId = 2188889901) {
|
|
513
534
|
return {
|
|
514
535
|
name: 'Sales read write',
|
|
515
536
|
enabled: true,
|
|
@@ -527,6 +548,12 @@ function buildAccessRuleShapeHint(userId = 2188889901) {
|
|
|
527
548
|
},
|
|
528
549
|
};
|
|
529
550
|
}
|
|
551
|
+
function buildAccessRuleCreateToolCall(body) {
|
|
552
|
+
return {
|
|
553
|
+
command: 'access-rule create',
|
|
554
|
+
args: ['--app-id', '<app_id>', '--table-id', '<table_id>', '--body-json', JSON.stringify(body)],
|
|
555
|
+
};
|
|
556
|
+
}
|
|
530
557
|
function buildAssignUsersValidationDetails(scope, command, requestType, path, message, env = process.env) {
|
|
531
558
|
return {
|
|
532
559
|
operation: `${scope}.${command.module}.${command.functionName}`,
|
|
@@ -664,7 +691,15 @@ function buildBodyGuidance(requestType) {
|
|
|
664
691
|
};
|
|
665
692
|
}
|
|
666
693
|
if (requestType === 'AppIngressCreateReqVO' || requestType === 'AppIngressUpdateReqVO') {
|
|
694
|
+
const retryToolCalls = [
|
|
695
|
+
buildAccessRuleCreateToolCall(buildFullAccessRuleBody()),
|
|
696
|
+
buildAccessRuleCreateToolCall(buildHiddenFieldAccessRuleBody()),
|
|
697
|
+
];
|
|
667
698
|
return {
|
|
699
|
+
retryToolCalls,
|
|
700
|
+
suggestions: [
|
|
701
|
+
'next arcubase-admin-cli input: retryToolCalls[0] for full access or retryToolCalls[1] for hidden-field access',
|
|
702
|
+
],
|
|
668
703
|
shapeHint: buildAccessRuleShapeHint(),
|
|
669
704
|
commonMistakes: [
|
|
670
705
|
'use options.policy, not top-level permissions',
|
|
@@ -938,6 +973,71 @@ function unwrapData(value) {
|
|
|
938
973
|
}
|
|
939
974
|
return current;
|
|
940
975
|
}
|
|
976
|
+
function numericFieldKeys(fields) {
|
|
977
|
+
if (!isRecord(fields))
|
|
978
|
+
return [];
|
|
979
|
+
return Object.keys(fields).filter((key) => /^\d+$/.test(key)).sort((left, right) => Number(left) - Number(right));
|
|
980
|
+
}
|
|
981
|
+
function permitFieldId(permit) {
|
|
982
|
+
if (!isRecord(permit) || typeof permit.key !== 'string')
|
|
983
|
+
return undefined;
|
|
984
|
+
const match = /^:(\d+)$/.exec(permit.key);
|
|
985
|
+
return match?.[1];
|
|
986
|
+
}
|
|
987
|
+
function rowFieldPermits(value) {
|
|
988
|
+
const data = unwrapData(value);
|
|
989
|
+
const fieldPermits = isRecord(data) && isRecord(data.fieldPermits) ? data.fieldPermits : undefined;
|
|
990
|
+
return fieldPermits && Array.isArray(fieldPermits.fields)
|
|
991
|
+
? fieldPermits.fields.filter((item) => isRecord(item))
|
|
992
|
+
: [];
|
|
993
|
+
}
|
|
994
|
+
function buildRowValueEvidence(command, responseBody) {
|
|
995
|
+
if (command.scope !== 'user' || command.commandPath[0] !== 'row')
|
|
996
|
+
return undefined;
|
|
997
|
+
const action = command.commandPath[1];
|
|
998
|
+
const data = unwrapData(responseBody);
|
|
999
|
+
const rules = [
|
|
1000
|
+
'current row values come only from row query fields or row get record.fields',
|
|
1001
|
+
'table schema fields[].value and default_value_mode are schema defaults, not returned row values',
|
|
1002
|
+
'a field id absent from returned row fields has no row-value evidence',
|
|
1003
|
+
'read visibility status is fieldPermits read:false; FIELD_PERMISSION_REQUIRED is only a write preflight error',
|
|
1004
|
+
];
|
|
1005
|
+
if (action === 'get') {
|
|
1006
|
+
const record = isRecord(data) && isRecord(data.record) ? data.record : undefined;
|
|
1007
|
+
const returnedFieldIds = numericFieldKeys(record?.fields);
|
|
1008
|
+
const permits = rowFieldPermits(responseBody);
|
|
1009
|
+
const permitFieldIds = permits.map(permitFieldId).filter((id) => Boolean(id));
|
|
1010
|
+
const deniedReadFieldIds = permits
|
|
1011
|
+
.filter((permit) => permit.read === false)
|
|
1012
|
+
.map(permitFieldId)
|
|
1013
|
+
.filter((id) => Boolean(id))
|
|
1014
|
+
.sort((left, right) => Number(left) - Number(right));
|
|
1015
|
+
const notReturnedFieldIds = permitFieldIds
|
|
1016
|
+
.filter((id) => !returnedFieldIds.includes(id))
|
|
1017
|
+
.sort((left, right) => Number(left) - Number(right));
|
|
1018
|
+
return {
|
|
1019
|
+
source: 'row get record.fields',
|
|
1020
|
+
returnedFieldIds,
|
|
1021
|
+
notReturnedFieldIds,
|
|
1022
|
+
deniedReadFieldIds,
|
|
1023
|
+
absentFieldMeaning: 'not returned to current user; no row-value evidence',
|
|
1024
|
+
rules,
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
if (action === 'query') {
|
|
1028
|
+
const items = isRecord(data) && Array.isArray(data.items) ? data.items : [];
|
|
1029
|
+
return {
|
|
1030
|
+
source: 'row query items[].fields',
|
|
1031
|
+
rows: items.filter((item) => isRecord(item)).map((item) => ({
|
|
1032
|
+
rowId: item.id,
|
|
1033
|
+
returnedFieldIds: numericFieldKeys(item.fields),
|
|
1034
|
+
})),
|
|
1035
|
+
absentFieldMeaning: 'not returned to current user; no row-value evidence',
|
|
1036
|
+
rules,
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
return undefined;
|
|
1040
|
+
}
|
|
941
1041
|
function requiredRowAction(command, flags) {
|
|
942
1042
|
if (command.commandPath[0] !== 'row')
|
|
943
1043
|
return undefined;
|
|
@@ -1272,7 +1372,7 @@ export async function executeCLI(scope, argv, env, fetchImpl = fetch) {
|
|
|
1272
1372
|
}
|
|
1273
1373
|
catch (error) {
|
|
1274
1374
|
if (error instanceof CLIError && error.code === 'INVALID_BODY_JSON' && command.requestType) {
|
|
1275
|
-
throw new CLIError('INVALID_BODY_JSON', `body json is invalid for ${scope}.${command.commandPath.join(' ')};
|
|
1375
|
+
throw new CLIError('INVALID_BODY_JSON', `body json is invalid for ${scope}.${command.commandPath.join(' ')}; retry with retryToolCalls[0] or retryToolCalls[1] when present`, 2, buildBodyValidationDetails(scope, command, command.requestType, [{ path: 'body', message: 'invalid JSON syntax' }], runtimeEnv));
|
|
1276
1376
|
}
|
|
1277
1377
|
throw error;
|
|
1278
1378
|
}
|
|
@@ -1381,10 +1481,12 @@ export async function executeCLI(scope, argv, env, fetchImpl = fetch) {
|
|
|
1381
1481
|
: {}),
|
|
1382
1482
|
});
|
|
1383
1483
|
}
|
|
1484
|
+
const rowValueEvidence = buildRowValueEvidence(command, parsedResponse);
|
|
1384
1485
|
return {
|
|
1385
1486
|
kind: 'result',
|
|
1386
1487
|
commandPath: command.commandPath,
|
|
1387
1488
|
status: response.status,
|
|
1388
1489
|
data: parsedResponse,
|
|
1490
|
+
...(rowValueEvidence ? { rowValueEvidence } : {}),
|
|
1389
1491
|
};
|
|
1390
1492
|
}
|
|
@@ -250,6 +250,37 @@ test('body json parse errors include command docs and type hints', async () => {
|
|
|
250
250
|
return true;
|
|
251
251
|
});
|
|
252
252
|
});
|
|
253
|
+
test('access-rule invalid json returns copyable create retry examples', async () => {
|
|
254
|
+
await assert.rejects(async () => {
|
|
255
|
+
await executeCLI('admin', ['access-rule', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-json', '{"name":"Admin full access","enabled":true,"type":"form","options":{"user_scope":[{"type":"user","id":2188889977}],"policy":{"key":"custom","actions":["view","add","edit","delete"],"all_fields_read":true,"all_fields_write":true},"list_options":{}}'], env, async () => new Response('{}', { status: 200 }));
|
|
256
|
+
}, (error) => {
|
|
257
|
+
assert.ok(error instanceof CLIError);
|
|
258
|
+
assert.equal(error.code, 'INVALID_BODY_JSON');
|
|
259
|
+
assert.equal(error.details?.requestType, 'AppIngressCreateReqVO');
|
|
260
|
+
assert.match(error.message, /retryToolCalls/);
|
|
261
|
+
const toolCall = (error.details?.retryToolCalls ?? [])[0];
|
|
262
|
+
assert.ok(toolCall);
|
|
263
|
+
assert.equal(toolCall.command, 'access-rule create');
|
|
264
|
+
assert.deepEqual(toolCall.args.slice(0, 5), ['--app-id', '<app_id>', '--table-id', '<table_id>', '--body-json']);
|
|
265
|
+
const body = JSON.parse(toolCall.args[5]);
|
|
266
|
+
assert.deepEqual(body, {
|
|
267
|
+
name: 'Admin full access',
|
|
268
|
+
enabled: true,
|
|
269
|
+
type: 'form',
|
|
270
|
+
options: {
|
|
271
|
+
user_scope: [{ type: 'user', id: 2188889977 }],
|
|
272
|
+
policy: {
|
|
273
|
+
key: 'custom',
|
|
274
|
+
actions: ['view', 'add', 'edit', 'delete'],
|
|
275
|
+
all_fields_read: true,
|
|
276
|
+
all_fields_write: true,
|
|
277
|
+
},
|
|
278
|
+
list_options: {},
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
return true;
|
|
282
|
+
});
|
|
283
|
+
});
|
|
253
284
|
test('access-rule list executes against external root path', async () => {
|
|
254
285
|
const out = await executeCLI('admin', ['access-rule', 'list', '--app-id', 'app_1', '--table-id', 'table_1'], env, async (url, init) => new Response(JSON.stringify({ url, method: init?.method }), { status: 200 }));
|
|
255
286
|
assert.equal(out.kind, 'result');
|
|
@@ -396,6 +427,37 @@ test('row get resolves internal policy_id as query parameter', async () => {
|
|
|
396
427
|
assert.equal(out.kind, 'result');
|
|
397
428
|
assert.equal(calls[0], 'https://arcubase.example.com/api/entity/2188900691/2188900694/row/4000000046?policy_id=policy_hash_1');
|
|
398
429
|
});
|
|
430
|
+
test('row get returns machine-readable row value evidence', async () => {
|
|
431
|
+
const out = await executeCLI('user', ['row', 'get', '--app-id', '2188900691', '--table-id', '2188900694', '--row-id', '4000000046'], env, withRowPolicyResolver('2188900694', ['view'], async () => new Response(JSON.stringify({
|
|
432
|
+
data: {
|
|
433
|
+
record: {
|
|
434
|
+
id: 4000000046,
|
|
435
|
+
fields: { 1001: '复测投票' },
|
|
436
|
+
},
|
|
437
|
+
fieldPermits: {
|
|
438
|
+
all_fields_read: false,
|
|
439
|
+
fields: [
|
|
440
|
+
{ key: ':1001', read: true, write: true, report: false },
|
|
441
|
+
{ key: ':1002', read: false, write: false, report: false },
|
|
442
|
+
{ key: ':1003', read: true, write: true, report: false },
|
|
443
|
+
],
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
}), { status: 200 })));
|
|
447
|
+
assert.deepEqual(out.rowValueEvidence, {
|
|
448
|
+
source: 'row get record.fields',
|
|
449
|
+
returnedFieldIds: ['1001'],
|
|
450
|
+
notReturnedFieldIds: ['1002', '1003'],
|
|
451
|
+
deniedReadFieldIds: ['1002'],
|
|
452
|
+
absentFieldMeaning: 'not returned to current user; no row-value evidence',
|
|
453
|
+
rules: [
|
|
454
|
+
'current row values come only from row query fields or row get record.fields',
|
|
455
|
+
'table schema fields[].value and default_value_mode are schema defaults, not returned row values',
|
|
456
|
+
'a field id absent from returned row fields has no row-value evidence',
|
|
457
|
+
'read visibility status is fieldPermits read:false; FIELD_PERMISSION_REQUIRED is only a write preflight error',
|
|
458
|
+
],
|
|
459
|
+
});
|
|
460
|
+
});
|
|
399
461
|
test('row command rejects user-provided policy_id', async () => {
|
|
400
462
|
const bodyFile = tempJSONFile({ fields: { 1001: '今天吃什么' }, policy_id: 'manual_policy' });
|
|
401
463
|
await assert.rejects(async () => {
|
package/dist/tests/help.test.js
CHANGED
|
@@ -81,6 +81,8 @@ test('admin access-rule help gives update whitelist and assign-users body-json e
|
|
|
81
81
|
assert.equal(create.kind, 'help');
|
|
82
82
|
assert.match(create.text, /Access rule/);
|
|
83
83
|
assert.match(create.text, /access-rule\.md/);
|
|
84
|
+
assert.match(create.text, /full access/);
|
|
85
|
+
assert.match(create.text, /"actions":\["view","add","edit","delete"\]/);
|
|
84
86
|
assert.match(create.text, /"user_scope":\[\{"type":"user","id":2188889901\}\]/);
|
|
85
87
|
assert.doesNotMatch(create.text, /"user_scope":\[\{"type":"member"/);
|
|
86
88
|
const update = await executeCLI('admin', ['access-rule', 'update', '--help'], env, async () => new Response('{}'));
|
package/package.json
CHANGED
|
@@ -20,6 +20,14 @@ Use the numeric `tenantUserId` / `TenantUser.ID`.
|
|
|
20
20
|
Do not use hash ids such as `arcubaseTenantUserId`.
|
|
21
21
|
Use `user_scope[].type:"user"` for a tenant user. Do not use FieldVO field type `member` in access rules.
|
|
22
22
|
|
|
23
|
+
## Body Input
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
--body-json '<json-string>' | --body-file access-rule.json
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Prefer `--body-json` for short payloads. `--body-file` is available for large or fragile JSON payloads.
|
|
30
|
+
|
|
23
31
|
## Create Read/Write Rule With Hidden Field
|
|
24
32
|
|
|
25
33
|
```bash
|
|
@@ -29,6 +37,17 @@ arcubase-admin access-rule create \
|
|
|
29
37
|
--body-json '{"name":"Sales read write","enabled":true,"type":"form","options":{"user_scope":[{"type":"user","id":2188889901}],"policy":{"key":"custom","actions":["view","add","edit"],"all_fields_read":false,"all_fields_write":false,"fields":[{"key":":1001","read":true,"write":true,"report":false},{"key":":1002","read":false,"write":false,"report":false},{"key":":1003","read":true,"write":true,"report":false}]},"list_options":{}}}'
|
|
30
38
|
```
|
|
31
39
|
|
|
40
|
+
## Create Admin Full Access Rule
|
|
41
|
+
|
|
42
|
+
Use this for the operator's own admin entry before creating seed rows.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
arcubase-admin access-rule create \
|
|
46
|
+
--app-id 2188893436 \
|
|
47
|
+
--table-id 2188893443 \
|
|
48
|
+
--body-json '{"name":"Admin full access","enabled":true,"type":"form","options":{"user_scope":[{"type":"user","id":2188889977}],"policy":{"key":"custom","actions":["view","add","edit","delete"],"all_fields_read":true,"all_fields_write":true},"list_options":{}}}'
|
|
49
|
+
```
|
|
50
|
+
|
|
32
51
|
Rules:
|
|
33
52
|
|
|
34
53
|
- read permission = `actions:["view"]`
|
|
@@ -83,6 +83,12 @@ Typical row shape:
|
|
|
83
83
|
}
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
+
Returned values:
|
|
87
|
+
|
|
88
|
+
- current row values are only in `fields`
|
|
89
|
+
- fields absent from `fields` are not returned to the current user and have no row-value evidence
|
|
90
|
+
- table schema `fields[].value` and `default_value_mode` describe schema defaults, not current row values
|
|
91
|
+
|
|
86
92
|
## Read one row
|
|
87
93
|
|
|
88
94
|
Command:
|
|
@@ -96,6 +102,13 @@ arcubase row get \
|
|
|
96
102
|
|
|
97
103
|
Use this when you already know the row id and want the full record payload.
|
|
98
104
|
|
|
105
|
+
Returned values:
|
|
106
|
+
|
|
107
|
+
- current row values are in `record.fields`
|
|
108
|
+
- fields absent from `record.fields` are not returned to the current user and have no row-value evidence
|
|
109
|
+
- use `fieldPermits` only to explain access, not as current row values
|
|
110
|
+
- read visibility status is `fieldPermits read:false`; `FIELD_PERMISSION_REQUIRED` is a write preflight error from row create/update
|
|
111
|
+
|
|
99
112
|
## Update a row
|
|
100
113
|
|
|
101
114
|
Command:
|
package/src/runtime/errors.ts
CHANGED
package/src/runtime/execute.ts
CHANGED
|
@@ -121,8 +121,9 @@ export function renderCommandHelp(scope: CommandScope, moduleName: string, comma
|
|
|
121
121
|
: []),
|
|
122
122
|
...(scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'create'
|
|
123
123
|
? [
|
|
124
|
-
'body-json
|
|
125
|
-
|
|
124
|
+
'body-json examples:',
|
|
125
|
+
` - full access: ${JSON.stringify(buildFullAccessRuleBody())}`,
|
|
126
|
+
` - hide field: ${JSON.stringify(buildHiddenFieldAccessRuleBody())}`,
|
|
126
127
|
'success requires:',
|
|
127
128
|
' - result.enabled must be true',
|
|
128
129
|
' - options.user_scope[].type is "user" for tenant users',
|
|
@@ -567,6 +568,28 @@ function isAccessRuleUpdateCommand(scope: CommandScope, command: NonNullable<Ret
|
|
|
567
568
|
}
|
|
568
569
|
|
|
569
570
|
function buildAccessRuleShapeHint(userId = 2188889901) {
|
|
571
|
+
return buildHiddenFieldAccessRuleBody(userId)
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function buildFullAccessRuleBody(userId = 2188889977) {
|
|
575
|
+
return {
|
|
576
|
+
name: 'Admin full access',
|
|
577
|
+
enabled: true,
|
|
578
|
+
type: 'form',
|
|
579
|
+
options: {
|
|
580
|
+
user_scope: [{ type: 'user', id: userId }],
|
|
581
|
+
policy: {
|
|
582
|
+
key: 'custom',
|
|
583
|
+
actions: ['view', 'add', 'edit', 'delete'],
|
|
584
|
+
all_fields_read: true,
|
|
585
|
+
all_fields_write: true,
|
|
586
|
+
},
|
|
587
|
+
list_options: {},
|
|
588
|
+
},
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function buildHiddenFieldAccessRuleBody(userId = 2188889901) {
|
|
570
593
|
return {
|
|
571
594
|
name: 'Sales read write',
|
|
572
595
|
enabled: true,
|
|
@@ -585,6 +608,13 @@ function buildAccessRuleShapeHint(userId = 2188889901) {
|
|
|
585
608
|
}
|
|
586
609
|
}
|
|
587
610
|
|
|
611
|
+
function buildAccessRuleCreateToolCall(body: unknown) {
|
|
612
|
+
return {
|
|
613
|
+
command: 'access-rule create',
|
|
614
|
+
args: ['--app-id', '<app_id>', '--table-id', '<table_id>', '--body-json', JSON.stringify(body)],
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
588
618
|
function buildAssignUsersValidationDetails(
|
|
589
619
|
scope: CommandScope,
|
|
590
620
|
command: NonNullable<ReturnType<typeof findCommand>>,
|
|
@@ -618,7 +648,7 @@ function buildAssignUsersValidationDetails(
|
|
|
618
648
|
}
|
|
619
649
|
}
|
|
620
650
|
|
|
621
|
-
function buildBodyGuidance(requestType: string): Pick<CLIErrorDetails, 'shapeHint' | 'commonMistakes' | 'suggestions'> {
|
|
651
|
+
function buildBodyGuidance(requestType: string): Pick<CLIErrorDetails, 'retryToolCalls' | 'shapeHint' | 'commonMistakes' | 'suggestions'> {
|
|
622
652
|
if (requestType === 'EntitySaveReqVO' || requestType === 'AppCreateEntityReqVO') {
|
|
623
653
|
return {
|
|
624
654
|
shapeHint: {
|
|
@@ -730,7 +760,15 @@ function buildBodyGuidance(requestType: string): Pick<CLIErrorDetails, 'shapeHin
|
|
|
730
760
|
}
|
|
731
761
|
}
|
|
732
762
|
if (requestType === 'AppIngressCreateReqVO' || requestType === 'AppIngressUpdateReqVO') {
|
|
763
|
+
const retryToolCalls = [
|
|
764
|
+
buildAccessRuleCreateToolCall(buildFullAccessRuleBody()),
|
|
765
|
+
buildAccessRuleCreateToolCall(buildHiddenFieldAccessRuleBody()),
|
|
766
|
+
]
|
|
733
767
|
return {
|
|
768
|
+
retryToolCalls,
|
|
769
|
+
suggestions: [
|
|
770
|
+
'next arcubase-admin-cli input: retryToolCalls[0] for full access or retryToolCalls[1] for hidden-field access',
|
|
771
|
+
],
|
|
734
772
|
shapeHint: buildAccessRuleShapeHint(),
|
|
735
773
|
commonMistakes: [
|
|
736
774
|
'use options.policy, not top-level permissions',
|
|
@@ -1233,6 +1271,72 @@ function unwrapData(value: unknown): unknown {
|
|
|
1233
1271
|
return current
|
|
1234
1272
|
}
|
|
1235
1273
|
|
|
1274
|
+
function numericFieldKeys(fields: unknown): string[] {
|
|
1275
|
+
if (!isRecord(fields)) return []
|
|
1276
|
+
return Object.keys(fields).filter((key) => /^\d+$/.test(key)).sort((left, right) => Number(left) - Number(right))
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
function permitFieldId(permit: unknown): string | undefined {
|
|
1280
|
+
if (!isRecord(permit) || typeof permit.key !== 'string') return undefined
|
|
1281
|
+
const match = /^:(\d+)$/.exec(permit.key)
|
|
1282
|
+
return match?.[1]
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
function rowFieldPermits(value: unknown): Array<Record<string, unknown>> {
|
|
1286
|
+
const data = unwrapData(value)
|
|
1287
|
+
const fieldPermits = isRecord(data) && isRecord(data.fieldPermits) ? data.fieldPermits : undefined
|
|
1288
|
+
return fieldPermits && Array.isArray(fieldPermits.fields)
|
|
1289
|
+
? fieldPermits.fields.filter((item): item is Record<string, unknown> => isRecord(item))
|
|
1290
|
+
: []
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function buildRowValueEvidence(command: NonNullable<ReturnType<typeof findCommand>>, responseBody: unknown): Record<string, unknown> | undefined {
|
|
1294
|
+
if (command.scope !== 'user' || command.commandPath[0] !== 'row') return undefined
|
|
1295
|
+
const action = command.commandPath[1]
|
|
1296
|
+
const data = unwrapData(responseBody)
|
|
1297
|
+
const rules = [
|
|
1298
|
+
'current row values come only from row query fields or row get record.fields',
|
|
1299
|
+
'table schema fields[].value and default_value_mode are schema defaults, not returned row values',
|
|
1300
|
+
'a field id absent from returned row fields has no row-value evidence',
|
|
1301
|
+
'read visibility status is fieldPermits read:false; FIELD_PERMISSION_REQUIRED is only a write preflight error',
|
|
1302
|
+
]
|
|
1303
|
+
if (action === 'get') {
|
|
1304
|
+
const record = isRecord(data) && isRecord(data.record) ? data.record : undefined
|
|
1305
|
+
const returnedFieldIds = numericFieldKeys(record?.fields)
|
|
1306
|
+
const permits = rowFieldPermits(responseBody)
|
|
1307
|
+
const permitFieldIds = permits.map(permitFieldId).filter((id): id is string => Boolean(id))
|
|
1308
|
+
const deniedReadFieldIds = permits
|
|
1309
|
+
.filter((permit) => permit.read === false)
|
|
1310
|
+
.map(permitFieldId)
|
|
1311
|
+
.filter((id): id is string => Boolean(id))
|
|
1312
|
+
.sort((left, right) => Number(left) - Number(right))
|
|
1313
|
+
const notReturnedFieldIds = permitFieldIds
|
|
1314
|
+
.filter((id) => !returnedFieldIds.includes(id))
|
|
1315
|
+
.sort((left, right) => Number(left) - Number(right))
|
|
1316
|
+
return {
|
|
1317
|
+
source: 'row get record.fields',
|
|
1318
|
+
returnedFieldIds,
|
|
1319
|
+
notReturnedFieldIds,
|
|
1320
|
+
deniedReadFieldIds,
|
|
1321
|
+
absentFieldMeaning: 'not returned to current user; no row-value evidence',
|
|
1322
|
+
rules,
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
if (action === 'query') {
|
|
1326
|
+
const items = isRecord(data) && Array.isArray(data.items) ? data.items : []
|
|
1327
|
+
return {
|
|
1328
|
+
source: 'row query items[].fields',
|
|
1329
|
+
rows: items.filter((item): item is Record<string, unknown> => isRecord(item)).map((item) => ({
|
|
1330
|
+
rowId: item.id,
|
|
1331
|
+
returnedFieldIds: numericFieldKeys(item.fields),
|
|
1332
|
+
})),
|
|
1333
|
+
absentFieldMeaning: 'not returned to current user; no row-value evidence',
|
|
1334
|
+
rules,
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
return undefined
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1236
1340
|
function requiredRowAction(command: NonNullable<ReturnType<typeof findCommand>>, flags: Record<string, string | boolean>): string | undefined {
|
|
1237
1341
|
if (command.commandPath[0] !== 'row') return undefined
|
|
1238
1342
|
switch (command.commandPath[1]) {
|
|
@@ -1632,7 +1736,7 @@ export async function executeCLI(scope: CommandScope, argv: string[], env?: Runt
|
|
|
1632
1736
|
if (error instanceof CLIError && error.code === 'INVALID_BODY_JSON' && command.requestType) {
|
|
1633
1737
|
throw new CLIError(
|
|
1634
1738
|
'INVALID_BODY_JSON',
|
|
1635
|
-
`body json is invalid for ${scope}.${command.commandPath.join(' ')};
|
|
1739
|
+
`body json is invalid for ${scope}.${command.commandPath.join(' ')}; retry with retryToolCalls[0] or retryToolCalls[1] when present`,
|
|
1636
1740
|
2,
|
|
1637
1741
|
buildBodyValidationDetails(scope, command, command.requestType, [{ path: 'body', message: 'invalid JSON syntax' }], runtimeEnv),
|
|
1638
1742
|
)
|
|
@@ -1782,10 +1886,12 @@ export async function executeCLI(scope: CommandScope, argv: string[], env?: Runt
|
|
|
1782
1886
|
: {}),
|
|
1783
1887
|
})
|
|
1784
1888
|
}
|
|
1889
|
+
const rowValueEvidence = buildRowValueEvidence(command, parsedResponse)
|
|
1785
1890
|
return {
|
|
1786
1891
|
kind: 'result',
|
|
1787
1892
|
commandPath: command.commandPath,
|
|
1788
1893
|
status: response.status,
|
|
1789
1894
|
data: parsedResponse,
|
|
1895
|
+
...(rowValueEvidence ? { rowValueEvidence } : {}),
|
|
1790
1896
|
}
|
|
1791
1897
|
}
|
|
@@ -281,6 +281,38 @@ test('body json parse errors include command docs and type hints', async () => {
|
|
|
281
281
|
})
|
|
282
282
|
})
|
|
283
283
|
|
|
284
|
+
test('access-rule invalid json returns copyable create retry examples', async () => {
|
|
285
|
+
await assert.rejects(async () => {
|
|
286
|
+
await executeCLI('admin', ['access-rule', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-json', '{"name":"Admin full access","enabled":true,"type":"form","options":{"user_scope":[{"type":"user","id":2188889977}],"policy":{"key":"custom","actions":["view","add","edit","delete"],"all_fields_read":true,"all_fields_write":true},"list_options":{}}'], env as any, async () => new Response('{}', { status: 200 }))
|
|
287
|
+
}, (error: unknown) => {
|
|
288
|
+
assert.ok(error instanceof CLIError)
|
|
289
|
+
assert.equal(error.code, 'INVALID_BODY_JSON')
|
|
290
|
+
assert.equal(error.details?.requestType, 'AppIngressCreateReqVO')
|
|
291
|
+
assert.match(error.message, /retryToolCalls/)
|
|
292
|
+
const toolCall = (error.details?.retryToolCalls ?? [])[0] as any
|
|
293
|
+
assert.ok(toolCall)
|
|
294
|
+
assert.equal(toolCall.command, 'access-rule create')
|
|
295
|
+
assert.deepEqual(toolCall.args.slice(0, 5), ['--app-id', '<app_id>', '--table-id', '<table_id>', '--body-json'])
|
|
296
|
+
const body = JSON.parse(toolCall.args[5])
|
|
297
|
+
assert.deepEqual(body, {
|
|
298
|
+
name: 'Admin full access',
|
|
299
|
+
enabled: true,
|
|
300
|
+
type: 'form',
|
|
301
|
+
options: {
|
|
302
|
+
user_scope: [{ type: 'user', id: 2188889977 }],
|
|
303
|
+
policy: {
|
|
304
|
+
key: 'custom',
|
|
305
|
+
actions: ['view', 'add', 'edit', 'delete'],
|
|
306
|
+
all_fields_read: true,
|
|
307
|
+
all_fields_write: true,
|
|
308
|
+
},
|
|
309
|
+
list_options: {},
|
|
310
|
+
},
|
|
311
|
+
})
|
|
312
|
+
return true
|
|
313
|
+
})
|
|
314
|
+
})
|
|
315
|
+
|
|
284
316
|
test('access-rule list executes against external root path', async () => {
|
|
285
317
|
const out = await executeCLI(
|
|
286
318
|
'admin',
|
|
@@ -477,6 +509,43 @@ test('row get resolves internal policy_id as query parameter', async () => {
|
|
|
477
509
|
assert.equal(calls[0], 'https://arcubase.example.com/api/entity/2188900691/2188900694/row/4000000046?policy_id=policy_hash_1')
|
|
478
510
|
})
|
|
479
511
|
|
|
512
|
+
test('row get returns machine-readable row value evidence', async () => {
|
|
513
|
+
const out = await executeCLI(
|
|
514
|
+
'user',
|
|
515
|
+
['row', 'get', '--app-id', '2188900691', '--table-id', '2188900694', '--row-id', '4000000046'],
|
|
516
|
+
env as any,
|
|
517
|
+
withRowPolicyResolver('2188900694', ['view'], async () => new Response(JSON.stringify({
|
|
518
|
+
data: {
|
|
519
|
+
record: {
|
|
520
|
+
id: 4000000046,
|
|
521
|
+
fields: { 1001: '复测投票' },
|
|
522
|
+
},
|
|
523
|
+
fieldPermits: {
|
|
524
|
+
all_fields_read: false,
|
|
525
|
+
fields: [
|
|
526
|
+
{ key: ':1001', read: true, write: true, report: false },
|
|
527
|
+
{ key: ':1002', read: false, write: false, report: false },
|
|
528
|
+
{ key: ':1003', read: true, write: true, report: false },
|
|
529
|
+
],
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
}), { status: 200 })),
|
|
533
|
+
)
|
|
534
|
+
assert.deepEqual(out.rowValueEvidence, {
|
|
535
|
+
source: 'row get record.fields',
|
|
536
|
+
returnedFieldIds: ['1001'],
|
|
537
|
+
notReturnedFieldIds: ['1002', '1003'],
|
|
538
|
+
deniedReadFieldIds: ['1002'],
|
|
539
|
+
absentFieldMeaning: 'not returned to current user; no row-value evidence',
|
|
540
|
+
rules: [
|
|
541
|
+
'current row values come only from row query fields or row get record.fields',
|
|
542
|
+
'table schema fields[].value and default_value_mode are schema defaults, not returned row values',
|
|
543
|
+
'a field id absent from returned row fields has no row-value evidence',
|
|
544
|
+
'read visibility status is fieldPermits read:false; FIELD_PERMISSION_REQUIRED is only a write preflight error',
|
|
545
|
+
],
|
|
546
|
+
})
|
|
547
|
+
})
|
|
548
|
+
|
|
480
549
|
test('row command rejects user-provided policy_id', async () => {
|
|
481
550
|
const bodyFile = tempJSONFile({ fields: { 1001: '今天吃什么' }, policy_id: 'manual_policy' })
|
|
482
551
|
await assert.rejects(async () => {
|
package/src/tests/help.test.ts
CHANGED
|
@@ -90,6 +90,8 @@ test('admin access-rule help gives update whitelist and assign-users body-json e
|
|
|
90
90
|
assert.equal(create.kind, 'help')
|
|
91
91
|
assert.match(create.text, /Access rule/)
|
|
92
92
|
assert.match(create.text, /access-rule\.md/)
|
|
93
|
+
assert.match(create.text, /full access/)
|
|
94
|
+
assert.match(create.text, /"actions":\["view","add","edit","delete"\]/)
|
|
93
95
|
assert.match(create.text, /"user_scope":\[\{"type":"user","id":2188889901\}\]/)
|
|
94
96
|
assert.doesNotMatch(create.text, /"user_scope":\[\{"type":"member"/)
|
|
95
97
|
|