@carthooks/arcubase-cli 0.1.9 → 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 +99 -11
- package/src/tests/execute_validation.test.ts +73 -15
- package/src/tests/help.test.ts +4 -1
|
@@ -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'
|
|
@@ -98,16 +98,19 @@ export function renderCommandHelp(scope: CommandScope, moduleName: string, comma
|
|
|
98
98
|
'body-json examples:',
|
|
99
99
|
' - enable: {"update":["enabled"],"enabled":true}',
|
|
100
100
|
' - rename: {"update":["name"],"name":"Sales read only"}',
|
|
101
|
-
' - update options: {"update":["options"],"options":{"user_scope":[{"type":"
|
|
101
|
+
' - update options: {"update":["options"],"options":{"user_scope":[{"type":"member","id":123}]}}',
|
|
102
102
|
]
|
|
103
103
|
: []),
|
|
104
104
|
...(scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'assign-users'
|
|
105
105
|
? [
|
|
106
106
|
'body-json example:',
|
|
107
|
-
' - {"update":["user_scope"],"options":{"user_scope":[{"type":"
|
|
107
|
+
' - {"update":["user_scope"],"options":{"user_scope":[{"type":"member","id":123}]}}',
|
|
108
108
|
'success requires:',
|
|
109
109
|
' - result.enabled must be true',
|
|
110
|
-
' - result.options.user_scope must contain the assigned
|
|
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',
|
|
111
114
|
]
|
|
112
115
|
: []),
|
|
113
116
|
...(docHints.length > 0
|
|
@@ -335,7 +338,46 @@ async function validateFileFieldPayloads(
|
|
|
335
338
|
}
|
|
336
339
|
}
|
|
337
340
|
|
|
338
|
-
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
|
+
}
|
|
339
381
|
throw new CLIError('BODY_VALIDATION_FAILED', message, 2, {
|
|
340
382
|
operation: `${scope}.${command.module}.${command.functionName}`,
|
|
341
383
|
requestType,
|
|
@@ -382,10 +424,10 @@ function validateActionSpecificRawBody(
|
|
|
382
424
|
return
|
|
383
425
|
}
|
|
384
426
|
|
|
385
|
-
if (scope
|
|
386
|
-
if ('users' in body || 'allowedUsers' in body) {
|
|
427
|
+
if (isAssignUsersCommand(scope, command)) {
|
|
428
|
+
if ('users' in body || 'allowedUsers' in body || 'user_scope' in body) {
|
|
387
429
|
throwBodyValidationFailure(
|
|
388
|
-
'access-rule assign-users
|
|
430
|
+
'access-rule assign-users only accepts body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"member","id":123}]}}',
|
|
389
431
|
command.requestType,
|
|
390
432
|
'body.options.user_scope',
|
|
391
433
|
scope,
|
|
@@ -411,13 +453,13 @@ function validateActionSpecificBody(
|
|
|
411
453
|
requireUpdateContains(scope, command, body, 'options')
|
|
412
454
|
}
|
|
413
455
|
|
|
414
|
-
if (scope
|
|
456
|
+
if (isAssignUsersCommand(scope, command)) {
|
|
415
457
|
const update = stringArray(body.update)
|
|
416
458
|
const options = isRecord(body.options) ? body.options : undefined
|
|
417
459
|
const userScope = options && Array.isArray(options.user_scope) ? options.user_scope : undefined
|
|
418
460
|
if ('users' in body || 'allowedUsers' in body) {
|
|
419
461
|
throwBodyValidationFailure(
|
|
420
|
-
'access-rule assign-users
|
|
462
|
+
'access-rule assign-users only accepts body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"member","id":123}]}}',
|
|
421
463
|
command.requestType,
|
|
422
464
|
'body.options.user_scope',
|
|
423
465
|
scope,
|
|
@@ -442,6 +484,36 @@ function validateActionSpecificBody(
|
|
|
442
484
|
command,
|
|
443
485
|
)
|
|
444
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
|
+
}
|
|
445
517
|
}
|
|
446
518
|
|
|
447
519
|
if (command.commandPath[0] === 'row' && command.commandPath[1] === 'selection-action') {
|
|
@@ -575,9 +647,25 @@ export async function executeCLI(scope: CommandScope, argv: string[], env?: Runt
|
|
|
575
647
|
if (schema) {
|
|
576
648
|
const parsedBody = schema.safeParse(body)
|
|
577
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
|
+
}
|
|
578
666
|
throw new CLIError(
|
|
579
667
|
'BODY_VALIDATION_FAILED',
|
|
580
|
-
|
|
668
|
+
message,
|
|
581
669
|
2,
|
|
582
670
|
{
|
|
583
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,
|
|
@@ -501,13 +526,7 @@ test('access-rule assign-users rejects users shortcut', async () => {
|
|
|
501
526
|
env as any,
|
|
502
527
|
async () => new Response('{}', { status: 200 }),
|
|
503
528
|
)
|
|
504
|
-
}, (error: unknown) =>
|
|
505
|
-
assert.ok(error instanceof CLIError)
|
|
506
|
-
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
507
|
-
assert.match(error.message, /options.user_scope/)
|
|
508
|
-
assert.equal(error.details?.requestType, 'AppIngressUpdateReqVO')
|
|
509
|
-
return true
|
|
510
|
-
})
|
|
529
|
+
}, (error: unknown) => assertAssignUsersGuidance(error, /options.user_scope/))
|
|
511
530
|
})
|
|
512
531
|
|
|
513
532
|
test('access-rule assign-users requires update user_scope and non-empty user_scope', async () => {
|
|
@@ -518,22 +537,61 @@ test('access-rule assign-users requires update user_scope and non-empty user_sco
|
|
|
518
537
|
env as any,
|
|
519
538
|
async () => new Response('{}', { status: 200 }),
|
|
520
539
|
)
|
|
521
|
-
}, (error: unknown) =>
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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/))
|
|
527
585
|
})
|
|
528
586
|
|
|
529
587
|
test('access-rule assign-users accepts canonical user_scope body', async () => {
|
|
530
588
|
const out = await executeCLI(
|
|
531
589
|
'admin',
|
|
532
|
-
['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":"
|
|
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}]}}'],
|
|
533
591
|
env as any,
|
|
534
592
|
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
535
593
|
)
|
|
536
594
|
assert.equal(out.kind, 'result')
|
|
537
595
|
assert.deepEqual(out.data.update, ['user_scope'])
|
|
538
|
-
assert.deepEqual(out.data.options.user_scope, [{ type: '
|
|
596
|
+
assert.deepEqual(out.data.options.user_scope, [{ type: 'member', id: 123 }])
|
|
539
597
|
})
|
package/src/tests/help.test.ts
CHANGED
|
@@ -76,5 +76,8 @@ test('admin access-rule help gives update whitelist and assign-users body-json e
|
|
|
76
76
|
const assign = await executeCLI('admin', ['access-rule', 'assign-users', '--help'], undefined, async () => new Response('{}'))
|
|
77
77
|
assert.equal(assign.kind, 'help')
|
|
78
78
|
assert.match(assign.text, /"update":\["user_scope"\]/)
|
|
79
|
-
assert.match(assign.text,
|
|
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"/)
|
|
80
83
|
})
|