@carthooks/arcubase-cli 0.1.8 → 0.1.10
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 +164 -1
- package/bundle/arcubase.mjs +164 -1
- package/dist/runtime/errors.d.ts +2 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/execute.d.ts.map +1 -1
- package/dist/runtime/execute.js +110 -1
- package/dist/tests/execute_validation.test.js +77 -0
- package/dist/tests/help.test.js +15 -0
- package/package.json +1 -1
- package/src/runtime/errors.ts +2 -0
- package/src/runtime/execute.ts +193 -3
- package/src/tests/execute_validation.test.ts +132 -0
- package/src/tests/help.test.ts +17 -0
|
@@ -6658,6 +6658,22 @@ function renderCommandHelp(scope, moduleName, commandName) {
|
|
|
6658
6658
|
" - row create: arcubase row create --app-id <app_id> --table-id <table_id> --body-file insert.json",
|
|
6659
6659
|
" - row query: arcubase row query --app-id <app_id> --table-id <table_id> --body-file query.json"
|
|
6660
6660
|
] : [],
|
|
6661
|
+
...scope === "admin" && command.commandPath[0] === "access-rule" && command.commandPath[1] === "update" ? [
|
|
6662
|
+
"body-json examples:",
|
|
6663
|
+
' - enable: {"update":["enabled"],"enabled":true}',
|
|
6664
|
+
' - rename: {"update":["name"],"name":"Sales read only"}',
|
|
6665
|
+
' - update options: {"update":["options"],"options":{"user_scope":[{"type":"member","id":123}]}}'
|
|
6666
|
+
] : [],
|
|
6667
|
+
...scope === "admin" && command.commandPath[0] === "access-rule" && command.commandPath[1] === "assign-users" ? [
|
|
6668
|
+
"body-json example:",
|
|
6669
|
+
' - {"update":["user_scope"],"options":{"user_scope":[{"type":"member","id":123}]}}',
|
|
6670
|
+
"success requires:",
|
|
6671
|
+
" - result.enabled must be true",
|
|
6672
|
+
" - result.options.user_scope must contain the assigned numeric TenantUser.ID values",
|
|
6673
|
+
" - PermitUserScope.id must be numeric TenantUser.ID",
|
|
6674
|
+
" - Do not copy hash arcubaseTenantUserId into PermitUserScope.id",
|
|
6675
|
+
" - If result.enabled is false, enable the rule first and retry assign-users"
|
|
6676
|
+
] : [],
|
|
6661
6677
|
...docHints.length > 0 ? ["docs:", ...docHints.map((item) => ` - ${item.title}: ${item.file}`)] : []
|
|
6662
6678
|
].join("\n");
|
|
6663
6679
|
}
|
|
@@ -6836,7 +6852,38 @@ async function validateFileFieldPayloads(scope, command, runtimeEnv, flags, body
|
|
|
6836
6852
|
}
|
|
6837
6853
|
}
|
|
6838
6854
|
}
|
|
6855
|
+
function isAssignUsersCommand(scope, command) {
|
|
6856
|
+
return scope === "admin" && command.commandPath[0] === "access-rule" && command.commandPath[1] === "assign-users";
|
|
6857
|
+
}
|
|
6858
|
+
function buildAssignUsersValidationDetails(scope, command, requestType, path2, message) {
|
|
6859
|
+
return {
|
|
6860
|
+
operation: `${scope}.${command.module}.${command.functionName}`,
|
|
6861
|
+
requestType,
|
|
6862
|
+
issues: [{ path: path2, message }],
|
|
6863
|
+
tsHints: [
|
|
6864
|
+
{ symbol: "AppIngressUpdateReqVO", file: "/opt/arcubase-sdk/types/ingress.ts" },
|
|
6865
|
+
{ symbol: "EntityIngressOptions", file: "/opt/arcubase-sdk/types/ingress.ts" },
|
|
6866
|
+
{ symbol: "PermitUserScope", file: "/opt/arcubase-sdk/types/common.ts" }
|
|
6867
|
+
],
|
|
6868
|
+
docHints: getDocHints(requestType),
|
|
6869
|
+
shapeHint: {
|
|
6870
|
+
update: ["user_scope"],
|
|
6871
|
+
options: {
|
|
6872
|
+
user_scope: [{ type: "member", id: 123 }]
|
|
6873
|
+
}
|
|
6874
|
+
},
|
|
6875
|
+
commonMistakes: [
|
|
6876
|
+
"do not use user_scope at top level",
|
|
6877
|
+
"do not use users or allowedUsers",
|
|
6878
|
+
"do not use hash tenant user id as id",
|
|
6879
|
+
"id must be numeric TenantUser.ID"
|
|
6880
|
+
]
|
|
6881
|
+
};
|
|
6882
|
+
}
|
|
6839
6883
|
function throwBodyValidationFailure(message, requestType, path2, scope, command) {
|
|
6884
|
+
if (isAssignUsersCommand(scope, command)) {
|
|
6885
|
+
throw new CLIError("BODY_VALIDATION_FAILED", message, 2, buildAssignUsersValidationDetails(scope, command, requestType, path2, message));
|
|
6886
|
+
}
|
|
6840
6887
|
throw new CLIError("BODY_VALIDATION_FAILED", message, 2, {
|
|
6841
6888
|
operation: `${scope}.${command.module}.${command.functionName}`,
|
|
6842
6889
|
requestType,
|
|
@@ -6850,10 +6897,109 @@ function throwBodyValidationFailure(message, requestType, path2, scope, command)
|
|
|
6850
6897
|
docHints: getDocHints(requestType)
|
|
6851
6898
|
});
|
|
6852
6899
|
}
|
|
6900
|
+
function stringArray(value) {
|
|
6901
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
6902
|
+
}
|
|
6903
|
+
function requireUpdateContains(scope, command, body, field) {
|
|
6904
|
+
if (!(field in body)) return;
|
|
6905
|
+
const update = stringArray(body.update);
|
|
6906
|
+
if (!update.includes(field)) {
|
|
6907
|
+
throwBodyValidationFailure(
|
|
6908
|
+
`body.update must include "${field}" when body.${field} is present. Example: {"update":["${field}"],"${field}":${field === "enabled" ? "true" : '"value"'}}`,
|
|
6909
|
+
command.requestType ?? "AppIngressUpdateReqVO",
|
|
6910
|
+
"body.update",
|
|
6911
|
+
scope,
|
|
6912
|
+
command
|
|
6913
|
+
);
|
|
6914
|
+
}
|
|
6915
|
+
}
|
|
6916
|
+
function validateActionSpecificRawBody(scope, command, body) {
|
|
6917
|
+
if (!isRecord2(body) || !command.requestType) {
|
|
6918
|
+
return;
|
|
6919
|
+
}
|
|
6920
|
+
if (isAssignUsersCommand(scope, command)) {
|
|
6921
|
+
if ("users" in body || "allowedUsers" in body || "user_scope" in body) {
|
|
6922
|
+
throwBodyValidationFailure(
|
|
6923
|
+
'access-rule assign-users only accepts body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"member","id":123}]}}',
|
|
6924
|
+
command.requestType,
|
|
6925
|
+
"body.options.user_scope",
|
|
6926
|
+
scope,
|
|
6927
|
+
command
|
|
6928
|
+
);
|
|
6929
|
+
}
|
|
6930
|
+
}
|
|
6931
|
+
}
|
|
6853
6932
|
function validateActionSpecificBody(scope, command, flags, body) {
|
|
6854
6933
|
if (!isRecord2(body) || !command.requestType) {
|
|
6855
6934
|
return;
|
|
6856
6935
|
}
|
|
6936
|
+
if (scope === "admin" && command.commandPath[0] === "access-rule" && command.commandPath[1] === "update") {
|
|
6937
|
+
requireUpdateContains(scope, command, body, "enabled");
|
|
6938
|
+
requireUpdateContains(scope, command, body, "name");
|
|
6939
|
+
requireUpdateContains(scope, command, body, "options");
|
|
6940
|
+
}
|
|
6941
|
+
if (isAssignUsersCommand(scope, command)) {
|
|
6942
|
+
const update = stringArray(body.update);
|
|
6943
|
+
const options = isRecord2(body.options) ? body.options : void 0;
|
|
6944
|
+
const userScope = options && Array.isArray(options.user_scope) ? options.user_scope : void 0;
|
|
6945
|
+
if ("users" in body || "allowedUsers" in body) {
|
|
6946
|
+
throwBodyValidationFailure(
|
|
6947
|
+
'access-rule assign-users only accepts body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"member","id":123}]}}',
|
|
6948
|
+
command.requestType,
|
|
6949
|
+
"body.options.user_scope",
|
|
6950
|
+
scope,
|
|
6951
|
+
command
|
|
6952
|
+
);
|
|
6953
|
+
}
|
|
6954
|
+
if (!update.includes("user_scope")) {
|
|
6955
|
+
throwBodyValidationFailure(
|
|
6956
|
+
'access-rule assign-users requires body.update to include "user_scope"',
|
|
6957
|
+
command.requestType,
|
|
6958
|
+
"body.update",
|
|
6959
|
+
scope,
|
|
6960
|
+
command
|
|
6961
|
+
);
|
|
6962
|
+
}
|
|
6963
|
+
if (!userScope || userScope.length === 0) {
|
|
6964
|
+
throwBodyValidationFailure(
|
|
6965
|
+
"access-rule assign-users requires non-empty body.options.user_scope",
|
|
6966
|
+
command.requestType,
|
|
6967
|
+
"body.options.user_scope",
|
|
6968
|
+
scope,
|
|
6969
|
+
command
|
|
6970
|
+
);
|
|
6971
|
+
}
|
|
6972
|
+
const assignedUserScope = userScope;
|
|
6973
|
+
for (const [index, item] of assignedUserScope.entries()) {
|
|
6974
|
+
if (!isRecord2(item)) {
|
|
6975
|
+
throwBodyValidationFailure(
|
|
6976
|
+
"access-rule assign-users requires each body.options.user_scope item to be an object",
|
|
6977
|
+
command.requestType,
|
|
6978
|
+
`body.options.user_scope.${index}`,
|
|
6979
|
+
scope,
|
|
6980
|
+
command
|
|
6981
|
+
);
|
|
6982
|
+
}
|
|
6983
|
+
if (item.type !== "member") {
|
|
6984
|
+
throwBodyValidationFailure(
|
|
6985
|
+
'access-rule assign-users requires body.options.user_scope[].type to be "member"',
|
|
6986
|
+
command.requestType,
|
|
6987
|
+
`body.options.user_scope.${index}.type`,
|
|
6988
|
+
scope,
|
|
6989
|
+
command
|
|
6990
|
+
);
|
|
6991
|
+
}
|
|
6992
|
+
if (typeof item.id !== "number" || !Number.isInteger(item.id) || item.id <= 0) {
|
|
6993
|
+
throwBodyValidationFailure(
|
|
6994
|
+
"access-rule assign-users requires body.options.user_scope[].id to be numeric TenantUser.ID",
|
|
6995
|
+
command.requestType,
|
|
6996
|
+
`body.options.user_scope.${index}.id`,
|
|
6997
|
+
scope,
|
|
6998
|
+
command
|
|
6999
|
+
);
|
|
7000
|
+
}
|
|
7001
|
+
}
|
|
7002
|
+
}
|
|
6857
7003
|
if (command.commandPath[0] === "row" && command.commandPath[1] === "selection-action") {
|
|
6858
7004
|
const action = flagValue(flags, "action");
|
|
6859
7005
|
const selection = body.selection;
|
|
@@ -6954,13 +7100,30 @@ async function executeCLI(scope, argv, env, fetchImpl = fetch) {
|
|
|
6954
7100
|
if (body === void 0) {
|
|
6955
7101
|
throw new CLIError("MISSING_BODY_FILE", `command requires --body-file or --body-json for ${command.requestType}`, 2);
|
|
6956
7102
|
}
|
|
7103
|
+
validateActionSpecificRawBody(scope, command, body);
|
|
6957
7104
|
const schema = getBodySchema(command.requestType);
|
|
6958
7105
|
if (schema) {
|
|
6959
7106
|
const parsedBody = schema.safeParse(body);
|
|
6960
7107
|
if (!parsedBody.success) {
|
|
7108
|
+
const message = parsedBody.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ");
|
|
7109
|
+
if (isAssignUsersCommand(scope, command)) {
|
|
7110
|
+
const hasUserScopeIDIssue = parsedBody.error.issues.some((issue) => issue.path.join(".") === "options.user_scope.0.id" || issue.path.join(".").startsWith("options.user_scope."));
|
|
7111
|
+
throw new CLIError(
|
|
7112
|
+
"BODY_VALIDATION_FAILED",
|
|
7113
|
+
hasUserScopeIDIssue ? "access-rule assign-users requires body.options.user_scope[].id to be numeric TenantUser.ID" : message,
|
|
7114
|
+
2,
|
|
7115
|
+
buildAssignUsersValidationDetails(
|
|
7116
|
+
scope,
|
|
7117
|
+
command,
|
|
7118
|
+
command.requestType,
|
|
7119
|
+
hasUserScopeIDIssue ? "body.options.user_scope" : "body",
|
|
7120
|
+
hasUserScopeIDIssue ? "access-rule assign-users requires body.options.user_scope[].id to be numeric TenantUser.ID" : message
|
|
7121
|
+
)
|
|
7122
|
+
);
|
|
7123
|
+
}
|
|
6961
7124
|
throw new CLIError(
|
|
6962
7125
|
"BODY_VALIDATION_FAILED",
|
|
6963
|
-
|
|
7126
|
+
message,
|
|
6964
7127
|
2,
|
|
6965
7128
|
{
|
|
6966
7129
|
operation: `${scope}.${command.module}.${command.functionName}`,
|
package/bundle/arcubase.mjs
CHANGED
|
@@ -6658,6 +6658,22 @@ function renderCommandHelp(scope, moduleName, commandName) {
|
|
|
6658
6658
|
" - row create: arcubase row create --app-id <app_id> --table-id <table_id> --body-file insert.json",
|
|
6659
6659
|
" - row query: arcubase row query --app-id <app_id> --table-id <table_id> --body-file query.json"
|
|
6660
6660
|
] : [],
|
|
6661
|
+
...scope === "admin" && command.commandPath[0] === "access-rule" && command.commandPath[1] === "update" ? [
|
|
6662
|
+
"body-json examples:",
|
|
6663
|
+
' - enable: {"update":["enabled"],"enabled":true}',
|
|
6664
|
+
' - rename: {"update":["name"],"name":"Sales read only"}',
|
|
6665
|
+
' - update options: {"update":["options"],"options":{"user_scope":[{"type":"member","id":123}]}}'
|
|
6666
|
+
] : [],
|
|
6667
|
+
...scope === "admin" && command.commandPath[0] === "access-rule" && command.commandPath[1] === "assign-users" ? [
|
|
6668
|
+
"body-json example:",
|
|
6669
|
+
' - {"update":["user_scope"],"options":{"user_scope":[{"type":"member","id":123}]}}',
|
|
6670
|
+
"success requires:",
|
|
6671
|
+
" - result.enabled must be true",
|
|
6672
|
+
" - result.options.user_scope must contain the assigned numeric TenantUser.ID values",
|
|
6673
|
+
" - PermitUserScope.id must be numeric TenantUser.ID",
|
|
6674
|
+
" - Do not copy hash arcubaseTenantUserId into PermitUserScope.id",
|
|
6675
|
+
" - If result.enabled is false, enable the rule first and retry assign-users"
|
|
6676
|
+
] : [],
|
|
6661
6677
|
...docHints.length > 0 ? ["docs:", ...docHints.map((item) => ` - ${item.title}: ${item.file}`)] : []
|
|
6662
6678
|
].join("\n");
|
|
6663
6679
|
}
|
|
@@ -6836,7 +6852,38 @@ async function validateFileFieldPayloads(scope, command, runtimeEnv, flags, body
|
|
|
6836
6852
|
}
|
|
6837
6853
|
}
|
|
6838
6854
|
}
|
|
6855
|
+
function isAssignUsersCommand(scope, command) {
|
|
6856
|
+
return scope === "admin" && command.commandPath[0] === "access-rule" && command.commandPath[1] === "assign-users";
|
|
6857
|
+
}
|
|
6858
|
+
function buildAssignUsersValidationDetails(scope, command, requestType, path2, message) {
|
|
6859
|
+
return {
|
|
6860
|
+
operation: `${scope}.${command.module}.${command.functionName}`,
|
|
6861
|
+
requestType,
|
|
6862
|
+
issues: [{ path: path2, message }],
|
|
6863
|
+
tsHints: [
|
|
6864
|
+
{ symbol: "AppIngressUpdateReqVO", file: "/opt/arcubase-sdk/types/ingress.ts" },
|
|
6865
|
+
{ symbol: "EntityIngressOptions", file: "/opt/arcubase-sdk/types/ingress.ts" },
|
|
6866
|
+
{ symbol: "PermitUserScope", file: "/opt/arcubase-sdk/types/common.ts" }
|
|
6867
|
+
],
|
|
6868
|
+
docHints: getDocHints(requestType),
|
|
6869
|
+
shapeHint: {
|
|
6870
|
+
update: ["user_scope"],
|
|
6871
|
+
options: {
|
|
6872
|
+
user_scope: [{ type: "member", id: 123 }]
|
|
6873
|
+
}
|
|
6874
|
+
},
|
|
6875
|
+
commonMistakes: [
|
|
6876
|
+
"do not use user_scope at top level",
|
|
6877
|
+
"do not use users or allowedUsers",
|
|
6878
|
+
"do not use hash tenant user id as id",
|
|
6879
|
+
"id must be numeric TenantUser.ID"
|
|
6880
|
+
]
|
|
6881
|
+
};
|
|
6882
|
+
}
|
|
6839
6883
|
function throwBodyValidationFailure(message, requestType, path2, scope, command) {
|
|
6884
|
+
if (isAssignUsersCommand(scope, command)) {
|
|
6885
|
+
throw new CLIError("BODY_VALIDATION_FAILED", message, 2, buildAssignUsersValidationDetails(scope, command, requestType, path2, message));
|
|
6886
|
+
}
|
|
6840
6887
|
throw new CLIError("BODY_VALIDATION_FAILED", message, 2, {
|
|
6841
6888
|
operation: `${scope}.${command.module}.${command.functionName}`,
|
|
6842
6889
|
requestType,
|
|
@@ -6850,10 +6897,109 @@ function throwBodyValidationFailure(message, requestType, path2, scope, command)
|
|
|
6850
6897
|
docHints: getDocHints(requestType)
|
|
6851
6898
|
});
|
|
6852
6899
|
}
|
|
6900
|
+
function stringArray(value) {
|
|
6901
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
6902
|
+
}
|
|
6903
|
+
function requireUpdateContains(scope, command, body, field) {
|
|
6904
|
+
if (!(field in body)) return;
|
|
6905
|
+
const update = stringArray(body.update);
|
|
6906
|
+
if (!update.includes(field)) {
|
|
6907
|
+
throwBodyValidationFailure(
|
|
6908
|
+
`body.update must include "${field}" when body.${field} is present. Example: {"update":["${field}"],"${field}":${field === "enabled" ? "true" : '"value"'}}`,
|
|
6909
|
+
command.requestType ?? "AppIngressUpdateReqVO",
|
|
6910
|
+
"body.update",
|
|
6911
|
+
scope,
|
|
6912
|
+
command
|
|
6913
|
+
);
|
|
6914
|
+
}
|
|
6915
|
+
}
|
|
6916
|
+
function validateActionSpecificRawBody(scope, command, body) {
|
|
6917
|
+
if (!isRecord2(body) || !command.requestType) {
|
|
6918
|
+
return;
|
|
6919
|
+
}
|
|
6920
|
+
if (isAssignUsersCommand(scope, command)) {
|
|
6921
|
+
if ("users" in body || "allowedUsers" in body || "user_scope" in body) {
|
|
6922
|
+
throwBodyValidationFailure(
|
|
6923
|
+
'access-rule assign-users only accepts body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"member","id":123}]}}',
|
|
6924
|
+
command.requestType,
|
|
6925
|
+
"body.options.user_scope",
|
|
6926
|
+
scope,
|
|
6927
|
+
command
|
|
6928
|
+
);
|
|
6929
|
+
}
|
|
6930
|
+
}
|
|
6931
|
+
}
|
|
6853
6932
|
function validateActionSpecificBody(scope, command, flags, body) {
|
|
6854
6933
|
if (!isRecord2(body) || !command.requestType) {
|
|
6855
6934
|
return;
|
|
6856
6935
|
}
|
|
6936
|
+
if (scope === "admin" && command.commandPath[0] === "access-rule" && command.commandPath[1] === "update") {
|
|
6937
|
+
requireUpdateContains(scope, command, body, "enabled");
|
|
6938
|
+
requireUpdateContains(scope, command, body, "name");
|
|
6939
|
+
requireUpdateContains(scope, command, body, "options");
|
|
6940
|
+
}
|
|
6941
|
+
if (isAssignUsersCommand(scope, command)) {
|
|
6942
|
+
const update = stringArray(body.update);
|
|
6943
|
+
const options = isRecord2(body.options) ? body.options : void 0;
|
|
6944
|
+
const userScope = options && Array.isArray(options.user_scope) ? options.user_scope : void 0;
|
|
6945
|
+
if ("users" in body || "allowedUsers" in body) {
|
|
6946
|
+
throwBodyValidationFailure(
|
|
6947
|
+
'access-rule assign-users only accepts body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"member","id":123}]}}',
|
|
6948
|
+
command.requestType,
|
|
6949
|
+
"body.options.user_scope",
|
|
6950
|
+
scope,
|
|
6951
|
+
command
|
|
6952
|
+
);
|
|
6953
|
+
}
|
|
6954
|
+
if (!update.includes("user_scope")) {
|
|
6955
|
+
throwBodyValidationFailure(
|
|
6956
|
+
'access-rule assign-users requires body.update to include "user_scope"',
|
|
6957
|
+
command.requestType,
|
|
6958
|
+
"body.update",
|
|
6959
|
+
scope,
|
|
6960
|
+
command
|
|
6961
|
+
);
|
|
6962
|
+
}
|
|
6963
|
+
if (!userScope || userScope.length === 0) {
|
|
6964
|
+
throwBodyValidationFailure(
|
|
6965
|
+
"access-rule assign-users requires non-empty body.options.user_scope",
|
|
6966
|
+
command.requestType,
|
|
6967
|
+
"body.options.user_scope",
|
|
6968
|
+
scope,
|
|
6969
|
+
command
|
|
6970
|
+
);
|
|
6971
|
+
}
|
|
6972
|
+
const assignedUserScope = userScope;
|
|
6973
|
+
for (const [index, item] of assignedUserScope.entries()) {
|
|
6974
|
+
if (!isRecord2(item)) {
|
|
6975
|
+
throwBodyValidationFailure(
|
|
6976
|
+
"access-rule assign-users requires each body.options.user_scope item to be an object",
|
|
6977
|
+
command.requestType,
|
|
6978
|
+
`body.options.user_scope.${index}`,
|
|
6979
|
+
scope,
|
|
6980
|
+
command
|
|
6981
|
+
);
|
|
6982
|
+
}
|
|
6983
|
+
if (item.type !== "member") {
|
|
6984
|
+
throwBodyValidationFailure(
|
|
6985
|
+
'access-rule assign-users requires body.options.user_scope[].type to be "member"',
|
|
6986
|
+
command.requestType,
|
|
6987
|
+
`body.options.user_scope.${index}.type`,
|
|
6988
|
+
scope,
|
|
6989
|
+
command
|
|
6990
|
+
);
|
|
6991
|
+
}
|
|
6992
|
+
if (typeof item.id !== "number" || !Number.isInteger(item.id) || item.id <= 0) {
|
|
6993
|
+
throwBodyValidationFailure(
|
|
6994
|
+
"access-rule assign-users requires body.options.user_scope[].id to be numeric TenantUser.ID",
|
|
6995
|
+
command.requestType,
|
|
6996
|
+
`body.options.user_scope.${index}.id`,
|
|
6997
|
+
scope,
|
|
6998
|
+
command
|
|
6999
|
+
);
|
|
7000
|
+
}
|
|
7001
|
+
}
|
|
7002
|
+
}
|
|
6857
7003
|
if (command.commandPath[0] === "row" && command.commandPath[1] === "selection-action") {
|
|
6858
7004
|
const action = flagValue(flags, "action");
|
|
6859
7005
|
const selection = body.selection;
|
|
@@ -6954,13 +7100,30 @@ async function executeCLI(scope, argv, env, fetchImpl = fetch) {
|
|
|
6954
7100
|
if (body === void 0) {
|
|
6955
7101
|
throw new CLIError("MISSING_BODY_FILE", `command requires --body-file or --body-json for ${command.requestType}`, 2);
|
|
6956
7102
|
}
|
|
7103
|
+
validateActionSpecificRawBody(scope, command, body);
|
|
6957
7104
|
const schema = getBodySchema(command.requestType);
|
|
6958
7105
|
if (schema) {
|
|
6959
7106
|
const parsedBody = schema.safeParse(body);
|
|
6960
7107
|
if (!parsedBody.success) {
|
|
7108
|
+
const message = parsedBody.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ");
|
|
7109
|
+
if (isAssignUsersCommand(scope, command)) {
|
|
7110
|
+
const hasUserScopeIDIssue = parsedBody.error.issues.some((issue) => issue.path.join(".") === "options.user_scope.0.id" || issue.path.join(".").startsWith("options.user_scope."));
|
|
7111
|
+
throw new CLIError(
|
|
7112
|
+
"BODY_VALIDATION_FAILED",
|
|
7113
|
+
hasUserScopeIDIssue ? "access-rule assign-users requires body.options.user_scope[].id to be numeric TenantUser.ID" : message,
|
|
7114
|
+
2,
|
|
7115
|
+
buildAssignUsersValidationDetails(
|
|
7116
|
+
scope,
|
|
7117
|
+
command,
|
|
7118
|
+
command.requestType,
|
|
7119
|
+
hasUserScopeIDIssue ? "body.options.user_scope" : "body",
|
|
7120
|
+
hasUserScopeIDIssue ? "access-rule assign-users requires body.options.user_scope[].id to be numeric TenantUser.ID" : message
|
|
7121
|
+
)
|
|
7122
|
+
);
|
|
7123
|
+
}
|
|
6961
7124
|
throw new CLIError(
|
|
6962
7125
|
"BODY_VALIDATION_FAILED",
|
|
6963
|
-
|
|
7126
|
+
message,
|
|
6964
7127
|
2,
|
|
6965
7128
|
{
|
|
6966
7129
|
operation: `${scope}.${command.module}.${command.functionName}`,
|
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,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,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,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 +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;AAYD,wBAAgB,cAAc,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAa1D;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAkBhF;AAOD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,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;AAYD,wBAAgB,cAAc,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAa1D;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAkBhF;AAOD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAkDvG;AA6bD,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,CAkLrI"}
|
package/dist/runtime/execute.js
CHANGED
|
@@ -72,6 +72,26 @@ export function renderCommandHelp(scope, moduleName, commandName) {
|
|
|
72
72
|
' - row query: arcubase row query --app-id <app_id> --table-id <table_id> --body-file query.json',
|
|
73
73
|
]
|
|
74
74
|
: []),
|
|
75
|
+
...(scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'update'
|
|
76
|
+
? [
|
|
77
|
+
'body-json examples:',
|
|
78
|
+
' - enable: {"update":["enabled"],"enabled":true}',
|
|
79
|
+
' - rename: {"update":["name"],"name":"Sales read only"}',
|
|
80
|
+
' - update options: {"update":["options"],"options":{"user_scope":[{"type":"member","id":123}]}}',
|
|
81
|
+
]
|
|
82
|
+
: []),
|
|
83
|
+
...(scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'assign-users'
|
|
84
|
+
? [
|
|
85
|
+
'body-json example:',
|
|
86
|
+
' - {"update":["user_scope"],"options":{"user_scope":[{"type":"member","id":123}]}}',
|
|
87
|
+
'success requires:',
|
|
88
|
+
' - result.enabled must be true',
|
|
89
|
+
' - result.options.user_scope must contain the assigned numeric TenantUser.ID values',
|
|
90
|
+
' - PermitUserScope.id must be numeric TenantUser.ID',
|
|
91
|
+
' - Do not copy hash arcubaseTenantUserId into PermitUserScope.id',
|
|
92
|
+
' - If result.enabled is false, enable the rule first and retry assign-users',
|
|
93
|
+
]
|
|
94
|
+
: []),
|
|
75
95
|
...(docHints.length > 0
|
|
76
96
|
? ['docs:', ...docHints.map((item) => ` - ${item.title}: ${item.file}`)]
|
|
77
97
|
: []),
|
|
@@ -269,7 +289,38 @@ async function validateFileFieldPayloads(scope, command, runtimeEnv, flags, body
|
|
|
269
289
|
}
|
|
270
290
|
}
|
|
271
291
|
}
|
|
292
|
+
function isAssignUsersCommand(scope, command) {
|
|
293
|
+
return scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'assign-users';
|
|
294
|
+
}
|
|
295
|
+
function buildAssignUsersValidationDetails(scope, command, requestType, path, message) {
|
|
296
|
+
return {
|
|
297
|
+
operation: `${scope}.${command.module}.${command.functionName}`,
|
|
298
|
+
requestType,
|
|
299
|
+
issues: [{ path, message }],
|
|
300
|
+
tsHints: [
|
|
301
|
+
{ symbol: 'AppIngressUpdateReqVO', file: '/opt/arcubase-sdk/types/ingress.ts' },
|
|
302
|
+
{ symbol: 'EntityIngressOptions', file: '/opt/arcubase-sdk/types/ingress.ts' },
|
|
303
|
+
{ symbol: 'PermitUserScope', file: '/opt/arcubase-sdk/types/common.ts' },
|
|
304
|
+
],
|
|
305
|
+
docHints: getDocHints(requestType),
|
|
306
|
+
shapeHint: {
|
|
307
|
+
update: ['user_scope'],
|
|
308
|
+
options: {
|
|
309
|
+
user_scope: [{ type: 'member', id: 123 }],
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
commonMistakes: [
|
|
313
|
+
'do not use user_scope at top level',
|
|
314
|
+
'do not use users or allowedUsers',
|
|
315
|
+
'do not use hash tenant user id as id',
|
|
316
|
+
'id must be numeric TenantUser.ID',
|
|
317
|
+
],
|
|
318
|
+
};
|
|
319
|
+
}
|
|
272
320
|
function throwBodyValidationFailure(message, requestType, path, scope, command) {
|
|
321
|
+
if (isAssignUsersCommand(scope, command)) {
|
|
322
|
+
throw new CLIError('BODY_VALIDATION_FAILED', message, 2, buildAssignUsersValidationDetails(scope, command, requestType, path, message));
|
|
323
|
+
}
|
|
273
324
|
throw new CLIError('BODY_VALIDATION_FAILED', message, 2, {
|
|
274
325
|
operation: `${scope}.${command.module}.${command.functionName}`,
|
|
275
326
|
requestType,
|
|
@@ -283,10 +334,62 @@ function throwBodyValidationFailure(message, requestType, path, scope, command)
|
|
|
283
334
|
docHints: getDocHints(requestType),
|
|
284
335
|
});
|
|
285
336
|
}
|
|
337
|
+
function stringArray(value) {
|
|
338
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
|
|
339
|
+
}
|
|
340
|
+
function requireUpdateContains(scope, command, body, field) {
|
|
341
|
+
if (!(field in body))
|
|
342
|
+
return;
|
|
343
|
+
const update = stringArray(body.update);
|
|
344
|
+
if (!update.includes(field)) {
|
|
345
|
+
throwBodyValidationFailure(`body.update must include "${field}" when body.${field} is present. Example: {"update":["${field}"],"${field}":${field === 'enabled' ? 'true' : '"value"'}}`, command.requestType ?? 'AppIngressUpdateReqVO', 'body.update', scope, command);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function validateActionSpecificRawBody(scope, command, body) {
|
|
349
|
+
if (!isRecord(body) || !command.requestType) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (isAssignUsersCommand(scope, command)) {
|
|
353
|
+
if ('users' in body || 'allowedUsers' in body || 'user_scope' in body) {
|
|
354
|
+
throwBodyValidationFailure('access-rule assign-users only accepts body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"member","id":123}]}}', command.requestType, 'body.options.user_scope', scope, command);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
286
358
|
function validateActionSpecificBody(scope, command, flags, body) {
|
|
287
359
|
if (!isRecord(body) || !command.requestType) {
|
|
288
360
|
return;
|
|
289
361
|
}
|
|
362
|
+
if (scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'update') {
|
|
363
|
+
requireUpdateContains(scope, command, body, 'enabled');
|
|
364
|
+
requireUpdateContains(scope, command, body, 'name');
|
|
365
|
+
requireUpdateContains(scope, command, body, 'options');
|
|
366
|
+
}
|
|
367
|
+
if (isAssignUsersCommand(scope, command)) {
|
|
368
|
+
const update = stringArray(body.update);
|
|
369
|
+
const options = isRecord(body.options) ? body.options : undefined;
|
|
370
|
+
const userScope = options && Array.isArray(options.user_scope) ? options.user_scope : undefined;
|
|
371
|
+
if ('users' in body || 'allowedUsers' in body) {
|
|
372
|
+
throwBodyValidationFailure('access-rule assign-users only accepts body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"member","id":123}]}}', command.requestType, 'body.options.user_scope', scope, command);
|
|
373
|
+
}
|
|
374
|
+
if (!update.includes('user_scope')) {
|
|
375
|
+
throwBodyValidationFailure('access-rule assign-users requires body.update to include "user_scope"', command.requestType, 'body.update', scope, command);
|
|
376
|
+
}
|
|
377
|
+
if (!userScope || userScope.length === 0) {
|
|
378
|
+
throwBodyValidationFailure('access-rule assign-users requires non-empty body.options.user_scope', command.requestType, 'body.options.user_scope', scope, command);
|
|
379
|
+
}
|
|
380
|
+
const assignedUserScope = userScope;
|
|
381
|
+
for (const [index, item] of assignedUserScope.entries()) {
|
|
382
|
+
if (!isRecord(item)) {
|
|
383
|
+
throwBodyValidationFailure('access-rule assign-users requires each body.options.user_scope item to be an object', command.requestType, `body.options.user_scope.${index}`, scope, command);
|
|
384
|
+
}
|
|
385
|
+
if (item.type !== 'member') {
|
|
386
|
+
throwBodyValidationFailure('access-rule assign-users requires body.options.user_scope[].type to be "member"', command.requestType, `body.options.user_scope.${index}.type`, scope, command);
|
|
387
|
+
}
|
|
388
|
+
if (typeof item.id !== 'number' || !Number.isInteger(item.id) || item.id <= 0) {
|
|
389
|
+
throwBodyValidationFailure('access-rule assign-users requires body.options.user_scope[].id to be numeric TenantUser.ID', command.requestType, `body.options.user_scope.${index}.id`, scope, command);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
290
393
|
if (command.commandPath[0] === 'row' && command.commandPath[1] === 'selection-action') {
|
|
291
394
|
const action = flagValue(flags, 'action');
|
|
292
395
|
const selection = body.selection;
|
|
@@ -379,11 +482,17 @@ export async function executeCLI(scope, argv, env, fetchImpl = fetch) {
|
|
|
379
482
|
if (body === undefined) {
|
|
380
483
|
throw new CLIError('MISSING_BODY_FILE', `command requires --body-file or --body-json for ${command.requestType}`, 2);
|
|
381
484
|
}
|
|
485
|
+
validateActionSpecificRawBody(scope, command, body);
|
|
382
486
|
const schema = getBodySchema(command.requestType);
|
|
383
487
|
if (schema) {
|
|
384
488
|
const parsedBody = schema.safeParse(body);
|
|
385
489
|
if (!parsedBody.success) {
|
|
386
|
-
|
|
490
|
+
const message = parsedBody.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`).join('; ');
|
|
491
|
+
if (isAssignUsersCommand(scope, command)) {
|
|
492
|
+
const hasUserScopeIDIssue = parsedBody.error.issues.some((issue) => issue.path.join('.') === 'options.user_scope.0.id' || issue.path.join('.').startsWith('options.user_scope.'));
|
|
493
|
+
throw new CLIError('BODY_VALIDATION_FAILED', hasUserScopeIDIssue ? 'access-rule assign-users requires body.options.user_scope[].id to be numeric TenantUser.ID' : message, 2, buildAssignUsersValidationDetails(scope, command, command.requestType, hasUserScopeIDIssue ? 'body.options.user_scope' : 'body', hasUserScopeIDIssue ? 'access-rule assign-users requires body.options.user_scope[].id to be numeric TenantUser.ID' : message));
|
|
494
|
+
}
|
|
495
|
+
throw new CLIError('BODY_VALIDATION_FAILED', message, 2, {
|
|
387
496
|
operation: `${scope}.${command.module}.${command.functionName}`,
|
|
388
497
|
requestType: command.requestType,
|
|
389
498
|
issues: parsedBody.error.issues.map((issue) => ({
|
|
@@ -19,6 +19,30 @@ function tempJSONFile(payload) {
|
|
|
19
19
|
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2));
|
|
20
20
|
return filePath;
|
|
21
21
|
}
|
|
22
|
+
function assertAssignUsersGuidance(error, messagePattern) {
|
|
23
|
+
assert.ok(error instanceof CLIError);
|
|
24
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
25
|
+
if (messagePattern) {
|
|
26
|
+
assert.match(error.message, messagePattern);
|
|
27
|
+
}
|
|
28
|
+
assert.equal(error.details?.requestType, 'AppIngressUpdateReqVO');
|
|
29
|
+
assert.deepEqual(error.details?.shapeHint, {
|
|
30
|
+
update: ['user_scope'],
|
|
31
|
+
options: {
|
|
32
|
+
user_scope: [{ type: 'member', id: 123 }],
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
assert.deepEqual(error.details?.commonMistakes, [
|
|
36
|
+
'do not use user_scope at top level',
|
|
37
|
+
'do not use users or allowedUsers',
|
|
38
|
+
'do not use hash tenant user id as id',
|
|
39
|
+
'id must be numeric TenantUser.ID',
|
|
40
|
+
]);
|
|
41
|
+
assert.ok(error.details?.tsHints?.some((item) => item.symbol === 'AppIngressUpdateReqVO' && item.file === '/opt/arcubase-sdk/types/ingress.ts'));
|
|
42
|
+
assert.ok(error.details?.tsHints?.some((item) => item.symbol === 'EntityIngressOptions' && item.file === '/opt/arcubase-sdk/types/ingress.ts'));
|
|
43
|
+
assert.ok(error.details?.tsHints?.some((item) => item.symbol === 'PermitUserScope' && item.file === '/opt/arcubase-sdk/types/common.ts'));
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
22
46
|
function validEntitySavePayload(overrides = {}) {
|
|
23
47
|
return {
|
|
24
48
|
id: 2188889845,
|
|
@@ -370,3 +394,56 @@ test('entry passes include_app_tables query flag', async () => {
|
|
|
370
394
|
assert.equal(out.kind, 'result');
|
|
371
395
|
assert.equal(out.data.url, 'https://arcubase.example.com/api/entry?include_app_tables=true');
|
|
372
396
|
});
|
|
397
|
+
test('access-rule update rejects enabled without update whitelist', async () => {
|
|
398
|
+
await assert.rejects(async () => {
|
|
399
|
+
await executeCLI('admin', ['access-rule', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"enabled":true}'], env, async () => new Response('{}', { status: 200 }));
|
|
400
|
+
}, (error) => {
|
|
401
|
+
assert.ok(error instanceof CLIError);
|
|
402
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
403
|
+
assert.match(error.message, /update.*enabled/);
|
|
404
|
+
assert.equal(error.details?.requestType, 'AppIngressUpdateReqVO');
|
|
405
|
+
assert.ok((error.details?.tsHints ?? []).some((item) => item.file === '/opt/arcubase-sdk/types/ingress.ts'));
|
|
406
|
+
return true;
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
test('access-rule update accepts enabled with update whitelist', async () => {
|
|
410
|
+
const out = await executeCLI('admin', ['access-rule', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["enabled"],"enabled":true}'], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
|
|
411
|
+
assert.equal(out.kind, 'result');
|
|
412
|
+
assert.deepEqual(out.data, { update: ['enabled'], enabled: true });
|
|
413
|
+
});
|
|
414
|
+
test('access-rule assign-users rejects users shortcut', async () => {
|
|
415
|
+
await assert.rejects(async () => {
|
|
416
|
+
await executeCLI('admin', ['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"users":["tu_1"]}'], env, async () => new Response('{}', { status: 200 }));
|
|
417
|
+
}, (error) => assertAssignUsersGuidance(error, /options.user_scope/));
|
|
418
|
+
});
|
|
419
|
+
test('access-rule assign-users requires update user_scope and non-empty user_scope', async () => {
|
|
420
|
+
await assert.rejects(async () => {
|
|
421
|
+
await executeCLI('admin', ['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["user_scope"],"options":{"user_scope":[]}}'], env, async () => new Response('{}', { status: 200 }));
|
|
422
|
+
}, (error) => assertAssignUsersGuidance(error, /non-empty/));
|
|
423
|
+
});
|
|
424
|
+
test('access-rule assign-users rejects allowedUsers shortcut', async () => {
|
|
425
|
+
await assert.rejects(async () => {
|
|
426
|
+
await executeCLI('admin', ['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"allowedUsers":["tu_1"]}'], env, async () => new Response('{}', { status: 200 }));
|
|
427
|
+
}, (error) => assertAssignUsersGuidance(error, /options.user_scope/));
|
|
428
|
+
});
|
|
429
|
+
test('access-rule assign-users rejects top-level user_scope', async () => {
|
|
430
|
+
await assert.rejects(async () => {
|
|
431
|
+
await executeCLI('admin', ['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"user_scope":[{"type":"member","id":123}]}'], env, async () => new Response('{}', { status: 200 }));
|
|
432
|
+
}, (error) => assertAssignUsersGuidance(error, /options.user_scope/));
|
|
433
|
+
});
|
|
434
|
+
test('access-rule assign-users rejects non-member scope type', async () => {
|
|
435
|
+
await assert.rejects(async () => {
|
|
436
|
+
await executeCLI('admin', ['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":123}]}}'], env, async () => new Response('{}', { status: 200 }));
|
|
437
|
+
}, (error) => assertAssignUsersGuidance(error, /type.*member/));
|
|
438
|
+
});
|
|
439
|
+
test('access-rule assign-users rejects hash tenant user id', async () => {
|
|
440
|
+
await assert.rejects(async () => {
|
|
441
|
+
await executeCLI('admin', ['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["user_scope"],"options":{"user_scope":[{"type":"member","id":"zW96OnE0kyJa3B2"}]}}'], env, async () => new Response('{}', { status: 200 }));
|
|
442
|
+
}, (error) => assertAssignUsersGuidance(error, /numeric TenantUser.ID/));
|
|
443
|
+
});
|
|
444
|
+
test('access-rule assign-users accepts canonical user_scope body', async () => {
|
|
445
|
+
const out = await executeCLI('admin', ['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["user_scope"],"options":{"user_scope":[{"type":"member","id":123}]}}'], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
|
|
446
|
+
assert.equal(out.kind, 'result');
|
|
447
|
+
assert.deepEqual(out.data.update, ['user_scope']);
|
|
448
|
+
assert.deepEqual(out.data.options.user_scope, [{ type: 'member', id: 123 }]);
|
|
449
|
+
});
|
package/dist/tests/help.test.js
CHANGED
|
@@ -58,3 +58,18 @@ test('admin table update-schema help uses table nouns and body-json', async () =
|
|
|
58
58
|
assert.doesNotMatch(out.text, /--entity-id/);
|
|
59
59
|
assert.doesNotMatch(out.text, /admin-save-entity/);
|
|
60
60
|
});
|
|
61
|
+
test('admin access-rule help gives update whitelist and assign-users body-json examples', async () => {
|
|
62
|
+
const update = await executeCLI('admin', ['access-rule', 'update', '--help'], undefined, async () => new Response('{}'));
|
|
63
|
+
assert.equal(update.kind, 'help');
|
|
64
|
+
assert.match(update.text, /--app-id/);
|
|
65
|
+
assert.match(update.text, /--table-id/);
|
|
66
|
+
assert.match(update.text, /--rule-id/);
|
|
67
|
+
assert.match(update.text, /"update":\["enabled"\]/);
|
|
68
|
+
const assign = await executeCLI('admin', ['access-rule', 'assign-users', '--help'], undefined, async () => new Response('{}'));
|
|
69
|
+
assert.equal(assign.kind, 'help');
|
|
70
|
+
assert.match(assign.text, /"update":\["user_scope"\]/);
|
|
71
|
+
assert.match(assign.text, /\{"update":\["user_scope"\],"options":\{"user_scope":\[\{"type":"member","id":123\}\]\}\}/);
|
|
72
|
+
assert.doesNotMatch(assign.text, /"type":"user"/);
|
|
73
|
+
assert.doesNotMatch(assign.text, /"users"/);
|
|
74
|
+
assert.doesNotMatch(assign.text, /"allowedUsers"/);
|
|
75
|
+
});
|
package/package.json
CHANGED
package/src/runtime/errors.ts
CHANGED
package/src/runtime/execute.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseCLIArgs, hasFlag, flagValue } from './argv.js'
|
|
2
2
|
import { loadRuntimeEnv, type RuntimeEnv } from './env.js'
|
|
3
|
-
import { CLIError } from './errors.js'
|
|
3
|
+
import { CLIError, type CLIErrorDetails } from './errors.js'
|
|
4
4
|
import { buildRequestHeaders, buildURL, normalizeExternalEndpoint } from './http.js'
|
|
5
5
|
import { listModules, listModuleCommands, findCommand, findCommandSuggestions, type CommandScope } from './command_registry.js'
|
|
6
6
|
import { loadBodyFromFlags, loadQueryFromFlags } from './body_loader.js'
|
|
@@ -93,6 +93,26 @@ export function renderCommandHelp(scope: CommandScope, moduleName: string, comma
|
|
|
93
93
|
' - row query: arcubase row query --app-id <app_id> --table-id <table_id> --body-file query.json',
|
|
94
94
|
]
|
|
95
95
|
: []),
|
|
96
|
+
...(scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'update'
|
|
97
|
+
? [
|
|
98
|
+
'body-json examples:',
|
|
99
|
+
' - enable: {"update":["enabled"],"enabled":true}',
|
|
100
|
+
' - rename: {"update":["name"],"name":"Sales read only"}',
|
|
101
|
+
' - update options: {"update":["options"],"options":{"user_scope":[{"type":"member","id":123}]}}',
|
|
102
|
+
]
|
|
103
|
+
: []),
|
|
104
|
+
...(scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'assign-users'
|
|
105
|
+
? [
|
|
106
|
+
'body-json example:',
|
|
107
|
+
' - {"update":["user_scope"],"options":{"user_scope":[{"type":"member","id":123}]}}',
|
|
108
|
+
'success requires:',
|
|
109
|
+
' - result.enabled must be true',
|
|
110
|
+
' - result.options.user_scope must contain the assigned numeric TenantUser.ID values',
|
|
111
|
+
' - PermitUserScope.id must be numeric TenantUser.ID',
|
|
112
|
+
' - Do not copy hash arcubaseTenantUserId into PermitUserScope.id',
|
|
113
|
+
' - If result.enabled is false, enable the rule first and retry assign-users',
|
|
114
|
+
]
|
|
115
|
+
: []),
|
|
96
116
|
...(docHints.length > 0
|
|
97
117
|
? ['docs:', ...docHints.map((item) => ` - ${item.title}: ${item.file}`)]
|
|
98
118
|
: []),
|
|
@@ -318,7 +338,46 @@ async function validateFileFieldPayloads(
|
|
|
318
338
|
}
|
|
319
339
|
}
|
|
320
340
|
|
|
321
|
-
function
|
|
341
|
+
function isAssignUsersCommand(scope: CommandScope, command: NonNullable<ReturnType<typeof findCommand>>): boolean {
|
|
342
|
+
return scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'assign-users'
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function buildAssignUsersValidationDetails(
|
|
346
|
+
scope: CommandScope,
|
|
347
|
+
command: NonNullable<ReturnType<typeof findCommand>>,
|
|
348
|
+
requestType: string,
|
|
349
|
+
path: string,
|
|
350
|
+
message: string,
|
|
351
|
+
): CLIErrorDetails {
|
|
352
|
+
return {
|
|
353
|
+
operation: `${scope}.${command.module}.${command.functionName}`,
|
|
354
|
+
requestType,
|
|
355
|
+
issues: [{ path, message }],
|
|
356
|
+
tsHints: [
|
|
357
|
+
{ symbol: 'AppIngressUpdateReqVO', file: '/opt/arcubase-sdk/types/ingress.ts' },
|
|
358
|
+
{ symbol: 'EntityIngressOptions', file: '/opt/arcubase-sdk/types/ingress.ts' },
|
|
359
|
+
{ symbol: 'PermitUserScope', file: '/opt/arcubase-sdk/types/common.ts' },
|
|
360
|
+
],
|
|
361
|
+
docHints: getDocHints(requestType),
|
|
362
|
+
shapeHint: {
|
|
363
|
+
update: ['user_scope'],
|
|
364
|
+
options: {
|
|
365
|
+
user_scope: [{ type: 'member', id: 123 }],
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
commonMistakes: [
|
|
369
|
+
'do not use user_scope at top level',
|
|
370
|
+
'do not use users or allowedUsers',
|
|
371
|
+
'do not use hash tenant user id as id',
|
|
372
|
+
'id must be numeric TenantUser.ID',
|
|
373
|
+
],
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function throwBodyValidationFailure(message: string, requestType: string, path: string, scope: CommandScope, command: NonNullable<ReturnType<typeof findCommand>>): never {
|
|
378
|
+
if (isAssignUsersCommand(scope, command)) {
|
|
379
|
+
throw new CLIError('BODY_VALIDATION_FAILED', message, 2, buildAssignUsersValidationDetails(scope, command, requestType, path, message))
|
|
380
|
+
}
|
|
322
381
|
throw new CLIError('BODY_VALIDATION_FAILED', message, 2, {
|
|
323
382
|
operation: `${scope}.${command.module}.${command.functionName}`,
|
|
324
383
|
requestType,
|
|
@@ -333,6 +392,51 @@ function throwBodyValidationFailure(message: string, requestType: string, path:
|
|
|
333
392
|
})
|
|
334
393
|
}
|
|
335
394
|
|
|
395
|
+
function stringArray(value: unknown): string[] {
|
|
396
|
+
return Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : []
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function requireUpdateContains(
|
|
400
|
+
scope: CommandScope,
|
|
401
|
+
command: NonNullable<ReturnType<typeof findCommand>>,
|
|
402
|
+
body: Record<string, unknown>,
|
|
403
|
+
field: string,
|
|
404
|
+
) {
|
|
405
|
+
if (!(field in body)) return
|
|
406
|
+
const update = stringArray(body.update)
|
|
407
|
+
if (!update.includes(field)) {
|
|
408
|
+
throwBodyValidationFailure(
|
|
409
|
+
`body.update must include "${field}" when body.${field} is present. Example: {"update":["${field}"],"${field}":${field === 'enabled' ? 'true' : '"value"'}}`,
|
|
410
|
+
command.requestType ?? 'AppIngressUpdateReqVO',
|
|
411
|
+
'body.update',
|
|
412
|
+
scope,
|
|
413
|
+
command,
|
|
414
|
+
)
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function validateActionSpecificRawBody(
|
|
419
|
+
scope: CommandScope,
|
|
420
|
+
command: NonNullable<ReturnType<typeof findCommand>>,
|
|
421
|
+
body: unknown,
|
|
422
|
+
) {
|
|
423
|
+
if (!isRecord(body) || !command.requestType) {
|
|
424
|
+
return
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (isAssignUsersCommand(scope, command)) {
|
|
428
|
+
if ('users' in body || 'allowedUsers' in body || 'user_scope' in body) {
|
|
429
|
+
throwBodyValidationFailure(
|
|
430
|
+
'access-rule assign-users only accepts body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"member","id":123}]}}',
|
|
431
|
+
command.requestType,
|
|
432
|
+
'body.options.user_scope',
|
|
433
|
+
scope,
|
|
434
|
+
command,
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
336
440
|
function validateActionSpecificBody(
|
|
337
441
|
scope: CommandScope,
|
|
338
442
|
command: NonNullable<ReturnType<typeof findCommand>>,
|
|
@@ -343,6 +447,75 @@ function validateActionSpecificBody(
|
|
|
343
447
|
return
|
|
344
448
|
}
|
|
345
449
|
|
|
450
|
+
if (scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'update') {
|
|
451
|
+
requireUpdateContains(scope, command, body, 'enabled')
|
|
452
|
+
requireUpdateContains(scope, command, body, 'name')
|
|
453
|
+
requireUpdateContains(scope, command, body, 'options')
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (isAssignUsersCommand(scope, command)) {
|
|
457
|
+
const update = stringArray(body.update)
|
|
458
|
+
const options = isRecord(body.options) ? body.options : undefined
|
|
459
|
+
const userScope = options && Array.isArray(options.user_scope) ? options.user_scope : undefined
|
|
460
|
+
if ('users' in body || 'allowedUsers' in body) {
|
|
461
|
+
throwBodyValidationFailure(
|
|
462
|
+
'access-rule assign-users only accepts body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"member","id":123}]}}',
|
|
463
|
+
command.requestType,
|
|
464
|
+
'body.options.user_scope',
|
|
465
|
+
scope,
|
|
466
|
+
command,
|
|
467
|
+
)
|
|
468
|
+
}
|
|
469
|
+
if (!update.includes('user_scope')) {
|
|
470
|
+
throwBodyValidationFailure(
|
|
471
|
+
'access-rule assign-users requires body.update to include "user_scope"',
|
|
472
|
+
command.requestType,
|
|
473
|
+
'body.update',
|
|
474
|
+
scope,
|
|
475
|
+
command,
|
|
476
|
+
)
|
|
477
|
+
}
|
|
478
|
+
if (!userScope || userScope.length === 0) {
|
|
479
|
+
throwBodyValidationFailure(
|
|
480
|
+
'access-rule assign-users requires non-empty body.options.user_scope',
|
|
481
|
+
command.requestType,
|
|
482
|
+
'body.options.user_scope',
|
|
483
|
+
scope,
|
|
484
|
+
command,
|
|
485
|
+
)
|
|
486
|
+
}
|
|
487
|
+
const assignedUserScope = userScope
|
|
488
|
+
for (const [index, item] of assignedUserScope.entries()) {
|
|
489
|
+
if (!isRecord(item)) {
|
|
490
|
+
throwBodyValidationFailure(
|
|
491
|
+
'access-rule assign-users requires each body.options.user_scope item to be an object',
|
|
492
|
+
command.requestType,
|
|
493
|
+
`body.options.user_scope.${index}`,
|
|
494
|
+
scope,
|
|
495
|
+
command,
|
|
496
|
+
)
|
|
497
|
+
}
|
|
498
|
+
if (item.type !== 'member') {
|
|
499
|
+
throwBodyValidationFailure(
|
|
500
|
+
'access-rule assign-users requires body.options.user_scope[].type to be "member"',
|
|
501
|
+
command.requestType,
|
|
502
|
+
`body.options.user_scope.${index}.type`,
|
|
503
|
+
scope,
|
|
504
|
+
command,
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
if (typeof item.id !== 'number' || !Number.isInteger(item.id) || item.id <= 0) {
|
|
508
|
+
throwBodyValidationFailure(
|
|
509
|
+
'access-rule assign-users requires body.options.user_scope[].id to be numeric TenantUser.ID',
|
|
510
|
+
command.requestType,
|
|
511
|
+
`body.options.user_scope.${index}.id`,
|
|
512
|
+
scope,
|
|
513
|
+
command,
|
|
514
|
+
)
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
346
519
|
if (command.commandPath[0] === 'row' && command.commandPath[1] === 'selection-action') {
|
|
347
520
|
const action = flagValue(flags, 'action')
|
|
348
521
|
const selection = body.selection
|
|
@@ -469,13 +642,30 @@ export async function executeCLI(scope: CommandScope, argv: string[], env?: Runt
|
|
|
469
642
|
if (body === undefined) {
|
|
470
643
|
throw new CLIError('MISSING_BODY_FILE', `command requires --body-file or --body-json for ${command.requestType}`, 2)
|
|
471
644
|
}
|
|
645
|
+
validateActionSpecificRawBody(scope, command, body)
|
|
472
646
|
const schema = getBodySchema(command.requestType)
|
|
473
647
|
if (schema) {
|
|
474
648
|
const parsedBody = schema.safeParse(body)
|
|
475
649
|
if (!parsedBody.success) {
|
|
650
|
+
const message = parsedBody.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`).join('; ')
|
|
651
|
+
if (isAssignUsersCommand(scope, command)) {
|
|
652
|
+
const hasUserScopeIDIssue = parsedBody.error.issues.some((issue) => issue.path.join('.') === 'options.user_scope.0.id' || issue.path.join('.').startsWith('options.user_scope.'))
|
|
653
|
+
throw new CLIError(
|
|
654
|
+
'BODY_VALIDATION_FAILED',
|
|
655
|
+
hasUserScopeIDIssue ? 'access-rule assign-users requires body.options.user_scope[].id to be numeric TenantUser.ID' : message,
|
|
656
|
+
2,
|
|
657
|
+
buildAssignUsersValidationDetails(
|
|
658
|
+
scope,
|
|
659
|
+
command,
|
|
660
|
+
command.requestType,
|
|
661
|
+
hasUserScopeIDIssue ? 'body.options.user_scope' : 'body',
|
|
662
|
+
hasUserScopeIDIssue ? 'access-rule assign-users requires body.options.user_scope[].id to be numeric TenantUser.ID' : message,
|
|
663
|
+
)
|
|
664
|
+
)
|
|
665
|
+
}
|
|
476
666
|
throw new CLIError(
|
|
477
667
|
'BODY_VALIDATION_FAILED',
|
|
478
|
-
|
|
668
|
+
message,
|
|
479
669
|
2,
|
|
480
670
|
{
|
|
481
671
|
operation: `${scope}.${command.module}.${command.functionName}`,
|
|
@@ -22,6 +22,31 @@ function tempJSONFile(payload: unknown): string {
|
|
|
22
22
|
return filePath
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function assertAssignUsersGuidance(error: unknown, messagePattern?: RegExp): boolean {
|
|
26
|
+
assert.ok(error instanceof CLIError)
|
|
27
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
28
|
+
if (messagePattern) {
|
|
29
|
+
assert.match(error.message, messagePattern)
|
|
30
|
+
}
|
|
31
|
+
assert.equal(error.details?.requestType, 'AppIngressUpdateReqVO')
|
|
32
|
+
assert.deepEqual(error.details?.shapeHint, {
|
|
33
|
+
update: ['user_scope'],
|
|
34
|
+
options: {
|
|
35
|
+
user_scope: [{ type: 'member', id: 123 }],
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
assert.deepEqual(error.details?.commonMistakes, [
|
|
39
|
+
'do not use user_scope at top level',
|
|
40
|
+
'do not use users or allowedUsers',
|
|
41
|
+
'do not use hash tenant user id as id',
|
|
42
|
+
'id must be numeric TenantUser.ID',
|
|
43
|
+
])
|
|
44
|
+
assert.ok(error.details?.tsHints?.some((item) => item.symbol === 'AppIngressUpdateReqVO' && item.file === '/opt/arcubase-sdk/types/ingress.ts'))
|
|
45
|
+
assert.ok(error.details?.tsHints?.some((item) => item.symbol === 'EntityIngressOptions' && item.file === '/opt/arcubase-sdk/types/ingress.ts'))
|
|
46
|
+
assert.ok(error.details?.tsHints?.some((item) => item.symbol === 'PermitUserScope' && item.file === '/opt/arcubase-sdk/types/common.ts'))
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
|
|
25
50
|
function validEntitySavePayload(overrides: Record<string, unknown> = {}) {
|
|
26
51
|
return {
|
|
27
52
|
id: 2188889845,
|
|
@@ -463,3 +488,110 @@ test('entry passes include_app_tables query flag', async () => {
|
|
|
463
488
|
assert.equal(out.kind, 'result')
|
|
464
489
|
assert.equal(out.data.url, 'https://arcubase.example.com/api/entry?include_app_tables=true')
|
|
465
490
|
})
|
|
491
|
+
|
|
492
|
+
test('access-rule update rejects enabled without update whitelist', async () => {
|
|
493
|
+
await assert.rejects(async () => {
|
|
494
|
+
await executeCLI(
|
|
495
|
+
'admin',
|
|
496
|
+
['access-rule', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"enabled":true}'],
|
|
497
|
+
env as any,
|
|
498
|
+
async () => new Response('{}', { status: 200 }),
|
|
499
|
+
)
|
|
500
|
+
}, (error: unknown) => {
|
|
501
|
+
assert.ok(error instanceof CLIError)
|
|
502
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
503
|
+
assert.match(error.message, /update.*enabled/)
|
|
504
|
+
assert.equal(error.details?.requestType, 'AppIngressUpdateReqVO')
|
|
505
|
+
assert.ok((error.details?.tsHints ?? []).some((item) => item.file === '/opt/arcubase-sdk/types/ingress.ts'))
|
|
506
|
+
return true
|
|
507
|
+
})
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
test('access-rule update accepts enabled with update whitelist', async () => {
|
|
511
|
+
const out = await executeCLI(
|
|
512
|
+
'admin',
|
|
513
|
+
['access-rule', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["enabled"],"enabled":true}'],
|
|
514
|
+
env as any,
|
|
515
|
+
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
516
|
+
)
|
|
517
|
+
assert.equal(out.kind, 'result')
|
|
518
|
+
assert.deepEqual(out.data, { update: ['enabled'], enabled: true })
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
test('access-rule assign-users rejects users shortcut', async () => {
|
|
522
|
+
await assert.rejects(async () => {
|
|
523
|
+
await executeCLI(
|
|
524
|
+
'admin',
|
|
525
|
+
['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"users":["tu_1"]}'],
|
|
526
|
+
env as any,
|
|
527
|
+
async () => new Response('{}', { status: 200 }),
|
|
528
|
+
)
|
|
529
|
+
}, (error: unknown) => assertAssignUsersGuidance(error, /options.user_scope/))
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
test('access-rule assign-users requires update user_scope and non-empty user_scope', async () => {
|
|
533
|
+
await assert.rejects(async () => {
|
|
534
|
+
await executeCLI(
|
|
535
|
+
'admin',
|
|
536
|
+
['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["user_scope"],"options":{"user_scope":[]}}'],
|
|
537
|
+
env as any,
|
|
538
|
+
async () => new Response('{}', { status: 200 }),
|
|
539
|
+
)
|
|
540
|
+
}, (error: unknown) => assertAssignUsersGuidance(error, /non-empty/))
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
test('access-rule assign-users rejects allowedUsers shortcut', async () => {
|
|
544
|
+
await assert.rejects(async () => {
|
|
545
|
+
await executeCLI(
|
|
546
|
+
'admin',
|
|
547
|
+
['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"allowedUsers":["tu_1"]}'],
|
|
548
|
+
env as any,
|
|
549
|
+
async () => new Response('{}', { status: 200 }),
|
|
550
|
+
)
|
|
551
|
+
}, (error: unknown) => assertAssignUsersGuidance(error, /options.user_scope/))
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
test('access-rule assign-users rejects top-level user_scope', async () => {
|
|
555
|
+
await assert.rejects(async () => {
|
|
556
|
+
await executeCLI(
|
|
557
|
+
'admin',
|
|
558
|
+
['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"user_scope":[{"type":"member","id":123}]}'],
|
|
559
|
+
env as any,
|
|
560
|
+
async () => new Response('{}', { status: 200 }),
|
|
561
|
+
)
|
|
562
|
+
}, (error: unknown) => assertAssignUsersGuidance(error, /options.user_scope/))
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
test('access-rule assign-users rejects non-member scope type', async () => {
|
|
566
|
+
await assert.rejects(async () => {
|
|
567
|
+
await executeCLI(
|
|
568
|
+
'admin',
|
|
569
|
+
['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":123}]}}'],
|
|
570
|
+
env as any,
|
|
571
|
+
async () => new Response('{}', { status: 200 }),
|
|
572
|
+
)
|
|
573
|
+
}, (error: unknown) => assertAssignUsersGuidance(error, /type.*member/))
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
test('access-rule assign-users rejects hash tenant user id', async () => {
|
|
577
|
+
await assert.rejects(async () => {
|
|
578
|
+
await executeCLI(
|
|
579
|
+
'admin',
|
|
580
|
+
['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["user_scope"],"options":{"user_scope":[{"type":"member","id":"zW96OnE0kyJa3B2"}]}}'],
|
|
581
|
+
env as any,
|
|
582
|
+
async () => new Response('{}', { status: 200 }),
|
|
583
|
+
)
|
|
584
|
+
}, (error: unknown) => assertAssignUsersGuidance(error, /numeric TenantUser.ID/))
|
|
585
|
+
})
|
|
586
|
+
|
|
587
|
+
test('access-rule assign-users accepts canonical user_scope body', async () => {
|
|
588
|
+
const out = await executeCLI(
|
|
589
|
+
'admin',
|
|
590
|
+
['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["user_scope"],"options":{"user_scope":[{"type":"member","id":123}]}}'],
|
|
591
|
+
env as any,
|
|
592
|
+
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
593
|
+
)
|
|
594
|
+
assert.equal(out.kind, 'result')
|
|
595
|
+
assert.deepEqual(out.data.update, ['user_scope'])
|
|
596
|
+
assert.deepEqual(out.data.options.user_scope, [{ type: 'member', id: 123 }])
|
|
597
|
+
})
|
package/src/tests/help.test.ts
CHANGED
|
@@ -64,3 +64,20 @@ test('admin table update-schema help uses table nouns and body-json', async () =
|
|
|
64
64
|
assert.doesNotMatch(out.text, /--entity-id/)
|
|
65
65
|
assert.doesNotMatch(out.text, /admin-save-entity/)
|
|
66
66
|
})
|
|
67
|
+
|
|
68
|
+
test('admin access-rule help gives update whitelist and assign-users body-json examples', async () => {
|
|
69
|
+
const update = await executeCLI('admin', ['access-rule', 'update', '--help'], undefined, async () => new Response('{}'))
|
|
70
|
+
assert.equal(update.kind, 'help')
|
|
71
|
+
assert.match(update.text, /--app-id/)
|
|
72
|
+
assert.match(update.text, /--table-id/)
|
|
73
|
+
assert.match(update.text, /--rule-id/)
|
|
74
|
+
assert.match(update.text, /"update":\["enabled"\]/)
|
|
75
|
+
|
|
76
|
+
const assign = await executeCLI('admin', ['access-rule', 'assign-users', '--help'], undefined, async () => new Response('{}'))
|
|
77
|
+
assert.equal(assign.kind, 'help')
|
|
78
|
+
assert.match(assign.text, /"update":\["user_scope"\]/)
|
|
79
|
+
assert.match(assign.text, /\{"update":\["user_scope"\],"options":\{"user_scope":\[\{"type":"member","id":123\}\]\}\}/)
|
|
80
|
+
assert.doesNotMatch(assign.text, /"type":"user"/)
|
|
81
|
+
assert.doesNotMatch(assign.text, /"users"/)
|
|
82
|
+
assert.doesNotMatch(assign.text, /"allowedUsers"/)
|
|
83
|
+
})
|