Refactor utils to share components (#3279)
* refactor: 💡 initial refactor for utils to share components * refactor: 💡 actions logic * Add missing copyright headers * refactor: 💡 simplify actions getter logic * refactor: 💡 convert output fields logic into a separate getter * test: 💍 fix unit test * fix: 🐛 bug with unknown fields lint error * refactor: 💡 pr feedback - simplify template check * refactor: 💡 remove unnecessary unit test file
This commit is contained in:
1 parent
878bd241f3
commit
7cddf7cb91
4 files changed
+428
-520
No files matched your search
@@ -3,19 +3,22 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
const GRANT_FIELDS = ['ids', 'type', 'actions', 'output_fields'];
|
||||
import {
|
||||
GRANT_FIELDS,
|
||||
TEMPLATE_RESOURCE_TYPES,
|
||||
normalizeGrantsSchema,
|
||||
parseGrantLine,
|
||||
getValidActions,
|
||||
getValidOutputFields,
|
||||
} from './grant-parser';
|
||||
|
||||
const ID_TEMPLATES = ['{{.User.Id}}', '{{.Account.Id}}'];
|
||||
const CRUDL_ACTIONS = new Set(['create', 'read', 'update', 'delete', 'list']);
|
||||
const TEMPLATE_RESOURCE_TYPES = {
|
||||
'{{.User.Id}}': 'user',
|
||||
'{{.Account.Id}}': 'account',
|
||||
'{{user.id}}': 'user',
|
||||
'{{account.id}}': 'account',
|
||||
};
|
||||
|
||||
const filterByPrefix = (values, partial) =>
|
||||
values.filter((value) => value.startsWith(partial));
|
||||
const withWildCard = (values = []) => ['*', ...new Set(values)];
|
||||
const parseIds = (idsValue = '') => idsValue.split(',').filter(Boolean);
|
||||
const withWildCard = (values = []) => ['*', ...new Set(values)];
|
||||
|
||||
const getNoSuggestionsOption = (noSuggestionsLabel) => ({
|
||||
label: noSuggestionsLabel,
|
||||
@@ -23,88 +26,15 @@ const getNoSuggestionsOption = (noSuggestionsLabel) => ({
|
||||
apply: () => {},
|
||||
});
|
||||
|
||||
const parseGrantFields = (lineText = '') =>
|
||||
lineText
|
||||
.split(';')
|
||||
.map((pair) => {
|
||||
const [fieldName, ...valueParts] = pair.split('=');
|
||||
|
||||
// There should only be one '=' in a valid pair, but if there are more,
|
||||
// we'll keep the value parts together and let the user know it's
|
||||
// invalid through validation rather than breaking the parsing
|
||||
return {
|
||||
fieldName: fieldName,
|
||||
fieldValue: valueParts.join('='),
|
||||
};
|
||||
})
|
||||
.filter(({ fieldName }) => fieldName);
|
||||
|
||||
const getGrantFieldValue = (parsedFields, targetFieldName) =>
|
||||
parsedFields.find(({ fieldName }) => fieldName === targetFieldName)
|
||||
?.fieldValue;
|
||||
|
||||
const parseGrantLine = (lineText = '') => {
|
||||
const parsedFields = parseGrantFields(lineText);
|
||||
const parseGrantLineForCompletions = (lineText = '') => {
|
||||
const { fields, fieldMap } = parseGrantLine(lineText);
|
||||
|
||||
return {
|
||||
parsedFields,
|
||||
idsValue: getGrantFieldValue(parsedFields, 'ids'),
|
||||
typeValue: getGrantFieldValue(parsedFields, 'type'),
|
||||
actionsValue: getGrantFieldValue(parsedFields, 'actions'),
|
||||
outputFieldsValue: getGrantFieldValue(parsedFields, 'output_fields'),
|
||||
};
|
||||
};
|
||||
|
||||
export const normalizeGrantsSchema = (grantsSchema) => {
|
||||
const resourceTypes = grantsSchema.resource_types ?? [];
|
||||
|
||||
const resourcesByType = resourceTypes.reduce((resources, resourceType) => {
|
||||
const collectionActions = resourceType.collection_actions ?? [];
|
||||
const idActions = resourceType.id_actions ?? [];
|
||||
const idPrefixes = resourceType.id_prefixes ?? [];
|
||||
|
||||
resources[resourceType.type] = {
|
||||
actions: [...collectionActions, ...idActions],
|
||||
collectionActions,
|
||||
idActions,
|
||||
idPrefixes,
|
||||
parentType: resourceType.parent_type,
|
||||
outputFields: resourceType.output_fields ?? [],
|
||||
};
|
||||
|
||||
return resources;
|
||||
}, {});
|
||||
|
||||
const childResourceTypesByParentType = resourceTypes.reduce(
|
||||
(childrenByParentType, resourceType) => {
|
||||
const parentType = resourceType.parent_type;
|
||||
|
||||
if (!parentType) {
|
||||
return childrenByParentType;
|
||||
}
|
||||
|
||||
childrenByParentType[parentType] = [
|
||||
...(childrenByParentType[parentType] ?? []),
|
||||
resourceType.type,
|
||||
];
|
||||
|
||||
return childrenByParentType;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const resourceTypesByIdPrefix = resourceTypes.flatMap((resourceType) =>
|
||||
(resourceType.id_prefixes ?? []).map((idPrefix) => ({
|
||||
idPrefix,
|
||||
type: resourceType.type,
|
||||
})),
|
||||
);
|
||||
|
||||
return {
|
||||
resourceTypes,
|
||||
resourcesByType,
|
||||
childResourceTypesByParentType,
|
||||
resourceTypesByIdPrefix,
|
||||
parsedFieldNames: fields.map(({ key }) => key),
|
||||
idsValue: fieldMap['ids'],
|
||||
typeValue: fieldMap['type'],
|
||||
actionsValue: fieldMap['actions'],
|
||||
outputFieldsValue: fieldMap['output_fields'],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -114,15 +44,15 @@ const getResourceTypesForId = (schema, id) => {
|
||||
return [templateType].filter((type) => schema.resourcesByType[type]);
|
||||
}
|
||||
|
||||
return id.includes('_')
|
||||
? [
|
||||
schema.resourceTypesByIdPrefix.find(
|
||||
({ idPrefix }) => id.split('_')[0] === idPrefix,
|
||||
)?.type,
|
||||
].filter(Boolean)
|
||||
: schema.resourceTypesByIdPrefix
|
||||
.filter(({ idPrefix }) => idPrefix.startsWith(id))
|
||||
.map(({ type }) => type);
|
||||
if (id.includes('_')) {
|
||||
const prefix = id.split('_')[0];
|
||||
const type = schema.resourceTypesByIdPrefix[prefix];
|
||||
return type ? [type] : [];
|
||||
}
|
||||
|
||||
return Object.entries(schema.resourceTypesByIdPrefix)
|
||||
.filter(([prefix]) => prefix.startsWith(id))
|
||||
.map(([, type]) => type);
|
||||
};
|
||||
|
||||
const getCompatibleResourceTypeForIds = (schema, idsValue) => {
|
||||
@@ -143,142 +73,30 @@ const getCompatibleResourceTypeForIds = (schema, idsValue) => {
|
||||
return firstResourceType;
|
||||
};
|
||||
|
||||
const getIdActions = (schema, idsValue) => {
|
||||
const matchedType = getCompatibleResourceTypeForIds(schema, idsValue);
|
||||
|
||||
if (!matchedType) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return schema.resourcesByType[matchedType]?.idActions;
|
||||
};
|
||||
|
||||
const getChildResourceValues = (schema, idsValue, getValues) => {
|
||||
const parentType = getCompatibleResourceTypeForIds(schema, idsValue);
|
||||
|
||||
if (
|
||||
!parentType ||
|
||||
!schema.childResourceTypesByParentType[parentType]?.length
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const childTypes = schema.childResourceTypesByParentType[parentType] ?? [];
|
||||
|
||||
return [
|
||||
...new Set(
|
||||
childTypes.flatMap((type) => getValues(schema.resourcesByType[type])),
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
const getChildResourceActions = (schema, idsValue) =>
|
||||
getChildResourceValues(
|
||||
schema,
|
||||
idsValue,
|
||||
(resource) => resource?.actions ?? [],
|
||||
);
|
||||
|
||||
const getChildResourceOutputFields = (schema, idsValue) =>
|
||||
getChildResourceValues(
|
||||
schema,
|
||||
idsValue,
|
||||
(resource) => resource?.outputFields ?? [],
|
||||
);
|
||||
|
||||
const getTypeOptions = (schema, idsValue) => {
|
||||
const getTypeOptions = (schema, idsValue, compatibleType) => {
|
||||
// If there is no ids value, we can only suggest top-level resource types that have collection actions
|
||||
if (!idsValue) {
|
||||
return schema.resourceTypes
|
||||
.filter(
|
||||
(resource) =>
|
||||
!resource.parent_type &&
|
||||
resource.collection_actions.some(
|
||||
(action) => action === 'list' || action === 'create',
|
||||
),
|
||||
)
|
||||
.map((resource) => resource.type);
|
||||
return schema.topLevelTypes;
|
||||
}
|
||||
|
||||
const hasSpecificIds = Boolean(idsValue) && !idsValue.includes('*');
|
||||
if (hasSpecificIds) {
|
||||
const parentType = getCompatibleResourceTypeForIds(schema, idsValue);
|
||||
|
||||
if (!parentType) {
|
||||
if (!compatibleType) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return schema.childResourceTypesByParentType[parentType] ?? [];
|
||||
return schema.childResourceTypesByParentType[compatibleType] ?? [];
|
||||
}
|
||||
|
||||
// Return the ones that have id based actions (currently only billing doesn't have them)
|
||||
return schema.resourceTypes
|
||||
.filter((resource) => resource.id_actions?.length > 0)
|
||||
.map((resource) => resource.type);
|
||||
};
|
||||
|
||||
const getActionOptions = (schema, typeValue, idsValue) => {
|
||||
const actionOptions = withWildCard(
|
||||
Object.values(schema.resourcesByType).flatMap(({ actions }) => actions),
|
||||
);
|
||||
|
||||
const hasSpecificIds = Boolean(idsValue) && !idsValue.includes('*');
|
||||
const hasOnlyType = Boolean(typeValue) && !idsValue;
|
||||
const selectedResource = schema.resourcesByType[typeValue];
|
||||
|
||||
// Just show ID specific actions as collection actions are invalid here
|
||||
if (!typeValue && hasSpecificIds) {
|
||||
const idActions = getIdActions(schema, idsValue);
|
||||
return idActions.length ? withWildCard(idActions) : [];
|
||||
}
|
||||
|
||||
if (!typeValue || (typeValue === '*' && idsValue === '*')) {
|
||||
return actionOptions;
|
||||
}
|
||||
|
||||
if (hasOnlyType) {
|
||||
if (selectedResource?.parentType) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return selectedResource?.collectionActions ?? [];
|
||||
}
|
||||
|
||||
if (hasSpecificIds) {
|
||||
// These are pinned IDs with a wildcard type
|
||||
if (typeValue === '*') {
|
||||
const childResourceActions = getChildResourceActions(schema, idsValue);
|
||||
|
||||
return childResourceActions.length
|
||||
? withWildCard(childResourceActions)
|
||||
: [];
|
||||
}
|
||||
|
||||
const matchedType = getCompatibleResourceTypeForIds(schema, idsValue);
|
||||
const isSelectedChildType = (
|
||||
schema.childResourceTypesByParentType[matchedType] ?? []
|
||||
).includes(typeValue);
|
||||
|
||||
// A specific ID paired with its own type is not a valid combination.
|
||||
// For pinned IDs, only child types are valid.
|
||||
if (!selectedResource || !isSelectedChildType) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Return all the actions for the selected type from a pinned ID
|
||||
return withWildCard([
|
||||
...selectedResource.collectionActions,
|
||||
...selectedResource.idActions,
|
||||
]);
|
||||
}
|
||||
|
||||
// Should be wildcard IDs with a specific type, so show all actions for that type
|
||||
return selectedResource ? withWildCard(selectedResource.actions) : [];
|
||||
return Object.entries(schema.resourcesByType)
|
||||
.filter(([, resource]) => resource.idActions.length > 0)
|
||||
.map(([type]) => type);
|
||||
};
|
||||
|
||||
export const analyzeGrantString = (grantsSchema, grantString = '') => {
|
||||
const schema = normalizeGrantsSchema(grantsSchema);
|
||||
const { idsValue, typeValue } = parseGrantLine(grantString);
|
||||
const { idsValue, typeValue } = parseGrantLineForCompletions(grantString);
|
||||
|
||||
const hasSpecificIds = Boolean(idsValue) && !idsValue.includes('*');
|
||||
const hasExplicitType = Boolean(typeValue) && typeValue !== '*';
|
||||
@@ -316,21 +134,27 @@ export const analyzeGrantString = (grantsSchema, grantString = '') => {
|
||||
detectedResourceType = childTypes.length ? childTypes : null;
|
||||
} else if (typeValue !== '*' && idsValue) {
|
||||
// No type field, infer resource type from the ID prefix
|
||||
detectedResourceType = getCompatibleResourceTypeForIds(schema, idsValue);
|
||||
detectedResourceType = compatibleIdsResourceType;
|
||||
}
|
||||
|
||||
const hasWildcardIds = idsValue === '*';
|
||||
const crudlOnly = hasWildcardIds && !hasExplicitType;
|
||||
|
||||
const actions =
|
||||
const actionOptions =
|
||||
idsValue || typeValue
|
||||
? getActionOptions(schema, typeValue, idsValue)
|
||||
.filter(
|
||||
(action) =>
|
||||
action !== '*' && (!crudlOnly || CRUDL_ACTIONS.has(action)),
|
||||
)
|
||||
.sort((left, right) => left.localeCompare(right))
|
||||
? getValidActions(schema, {
|
||||
typeValue,
|
||||
idsValue,
|
||||
idsWildcard: hasWildcardIds,
|
||||
idsKnownType: compatibleIdsResourceType,
|
||||
}).actions
|
||||
: [];
|
||||
const actions =
|
||||
actionOptions
|
||||
.filter(
|
||||
(action) => action !== '*' && (!crudlOnly || CRUDL_ACTIONS.has(action)),
|
||||
)
|
||||
.sort((left, right) => left.localeCompare(right)) ?? [];
|
||||
|
||||
return {
|
||||
actions,
|
||||
@@ -341,37 +165,6 @@ export const analyzeGrantString = (grantsSchema, grantString = '') => {
|
||||
};
|
||||
};
|
||||
|
||||
const getOutputFieldOptions = (schema, typeValue, idsValue) => {
|
||||
const selectedResource = schema.resourcesByType[typeValue];
|
||||
|
||||
if (typeValue && typeValue !== '*' && selectedResource) {
|
||||
return selectedResource.outputFields.length
|
||||
? withWildCard(selectedResource.outputFields)
|
||||
: ['*'];
|
||||
}
|
||||
|
||||
const hasSpecificIds = Boolean(idsValue) && !idsValue.includes('*');
|
||||
|
||||
// Pinned IDs with wildcard type
|
||||
if (typeValue === '*' && hasSpecificIds) {
|
||||
const childOutputFields = getChildResourceOutputFields(schema, idsValue);
|
||||
return childOutputFields.length ? withWildCard(childOutputFields) : ['*'];
|
||||
}
|
||||
|
||||
if (!typeValue && hasSpecificIds) {
|
||||
const matchedType = getCompatibleResourceTypeForIds(schema, idsValue);
|
||||
if (matchedType) {
|
||||
const resourceOutputFields =
|
||||
schema.resourcesByType[matchedType]?.outputFields ?? [];
|
||||
return resourceOutputFields.length
|
||||
? withWildCard(resourceOutputFields)
|
||||
: ['*'];
|
||||
}
|
||||
}
|
||||
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
const getIdLookupTypes = (schema, typeValue, enteredIds) => {
|
||||
if (enteredIds.length > 0) {
|
||||
const compatibleResourceType = getCompatibleResourceTypeForIds(
|
||||
@@ -407,8 +200,11 @@ async function grantCompletions(
|
||||
const line = context.state.doc.lineAt(context.pos);
|
||||
const beforeCursor = line.text.slice(0, context.pos - line.from);
|
||||
|
||||
const { parsedFields, idsValue, typeValue } = parseGrantLine(line.text);
|
||||
const parsedFieldNames = parsedFields.map(({ fieldName }) => fieldName);
|
||||
const { parsedFieldNames, idsValue, typeValue } =
|
||||
parseGrantLineForCompletions(line.text);
|
||||
const compatibleType = idsValue
|
||||
? getCompatibleResourceTypeForIds(schema, idsValue)
|
||||
: null;
|
||||
|
||||
// Check if we're typing a field name (e.g. "type=") by starting at the beginning or after a semicolon.
|
||||
// This allows us to provide field name suggestions when the user is typing a new field
|
||||
@@ -435,7 +231,11 @@ async function grantCompletions(
|
||||
const typeValueMatch = beforeCursor.match(/type=([a-z-*]*)$/);
|
||||
if (typeValueMatch) {
|
||||
const partial = typeValueMatch[1];
|
||||
const matchingTypeOptions = getTypeOptions(schema, idsValue);
|
||||
const matchingTypeOptions = getTypeOptions(
|
||||
schema,
|
||||
idsValue,
|
||||
compatibleType,
|
||||
);
|
||||
const typeOptions =
|
||||
partial === '' && matchingTypeOptions.length
|
||||
? withWildCard(matchingTypeOptions)
|
||||
@@ -525,10 +325,13 @@ async function grantCompletions(
|
||||
const partial = enteredActions.pop() ?? '';
|
||||
const hasEnteredActions = enteredActions.length > 0;
|
||||
|
||||
const options = filterByPrefix(
|
||||
getActionOptions(schema, typeValue, idsValue),
|
||||
partial,
|
||||
)
|
||||
const actionOptions = getValidActions(schema, {
|
||||
typeValue,
|
||||
idsValue,
|
||||
idsWildcard: idsValue?.includes('*'),
|
||||
idsKnownType: compatibleType,
|
||||
}).actions;
|
||||
const options = filterByPrefix(actionOptions, partial)
|
||||
.filter((action) => !hasEnteredActions || action !== '*')
|
||||
.filter((action) => !enteredActions.includes(action))
|
||||
.map((action) => ({
|
||||
@@ -554,10 +357,11 @@ async function grantCompletions(
|
||||
const partial = enteredOutputFields.pop() ?? '';
|
||||
const hasEnteredOutputFields = enteredOutputFields.length > 0;
|
||||
|
||||
const options = filterByPrefix(
|
||||
getOutputFieldOptions(schema, typeValue, idsValue),
|
||||
partial,
|
||||
)
|
||||
const { outputFields } = getValidOutputFields(schema, {
|
||||
typeValue,
|
||||
idsKnownType: compatibleType,
|
||||
});
|
||||
const options = filterByPrefix(outputFields, partial)
|
||||
.filter((value) => !hasEnteredOutputFields || value !== '*')
|
||||
.filter((value) => !enteredOutputFields.includes(value))
|
||||
.map((value) => ({
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
* Copyright IBM Corp. 2021, 2026
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
const GRANT_FIELDS = ['ids', 'type', 'actions', 'output_fields'];
|
||||
const ID_TEMPLATES = [
|
||||
'{{.Account.Id}}',
|
||||
'{{.User.Id}}',
|
||||
'{{user.id}}',
|
||||
'{{account.id}}',
|
||||
];
|
||||
import {
|
||||
GRANT_FIELDS,
|
||||
TEMPLATE_RESOURCE_TYPES,
|
||||
normalizeGrantsSchema,
|
||||
parseGrantLine,
|
||||
getValidActions,
|
||||
getValidOutputFields,
|
||||
} from './grant-parser';
|
||||
|
||||
const createDeleteTextAction = (name) => ({
|
||||
name,
|
||||
@@ -29,86 +29,6 @@ const createAddTextAction = (name) => ({
|
||||
},
|
||||
});
|
||||
|
||||
const normalizeGrantsSchema = (grantsSchema) => {
|
||||
const resourceTypes = grantsSchema?.resource_types ?? [];
|
||||
|
||||
const resourcesByType = resourceTypes.reduce((resources, resourceType) => {
|
||||
const collectionActions = resourceType.collection_actions ?? [];
|
||||
const idActions = resourceType.id_actions ?? [];
|
||||
const idPrefixes = resourceType.id_prefixes ?? [];
|
||||
|
||||
resources[resourceType.type] = {
|
||||
actions: [...collectionActions, ...idActions],
|
||||
collectionActions,
|
||||
idActions,
|
||||
idPrefixes,
|
||||
parentType: resourceType.parent_type,
|
||||
outputFields: resourceType.output_fields ?? [],
|
||||
};
|
||||
|
||||
return resources;
|
||||
}, {});
|
||||
|
||||
const validResourceTypes = resourceTypes.map((resource) => resource.type);
|
||||
validResourceTypes.push('*');
|
||||
|
||||
const allActions = new Set();
|
||||
Object.values(resourcesByType).forEach((resource) => {
|
||||
resource.actions.forEach((action) => allActions.add(action));
|
||||
});
|
||||
const validActions = Array.from(allActions);
|
||||
validActions.push('*');
|
||||
validActions.push('no-op');
|
||||
|
||||
const resourcesByPrefix = resourceTypes.reduce((map, resource) => {
|
||||
resource.id_prefixes?.forEach((prefix) => {
|
||||
map[prefix] = resource.type;
|
||||
});
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
let parentTypes = new Set(
|
||||
resourceTypes.filter((r) => r.parent_type).map((r) => r.parent_type),
|
||||
);
|
||||
parentTypes = Array.from(parentTypes);
|
||||
|
||||
const topLevelTypes = resourceTypes
|
||||
.filter((r) => {
|
||||
const hasCreateAction = r.collection_actions.includes('create');
|
||||
const hasListAction = r.collection_actions.includes('list');
|
||||
return !r.parent_type && (hasCreateAction || hasListAction);
|
||||
})
|
||||
.map((r) => r.type);
|
||||
|
||||
const childResourceTypesByParentType = resourceTypes.reduce(
|
||||
(childrenByParentType, resourceType) => {
|
||||
const parentType = resourceType.parent_type;
|
||||
|
||||
if (!parentType) {
|
||||
return childrenByParentType;
|
||||
}
|
||||
|
||||
childrenByParentType[parentType] = [
|
||||
...(childrenByParentType[parentType] ?? []),
|
||||
resourceType.type,
|
||||
];
|
||||
|
||||
return childrenByParentType;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
return {
|
||||
resourcesByType,
|
||||
validResourceTypes,
|
||||
validActions,
|
||||
resourcesByPrefix,
|
||||
childResourceTypesByParentType,
|
||||
parentTypes,
|
||||
topLevelTypes,
|
||||
};
|
||||
};
|
||||
|
||||
const grantLinter = (context, schema, translate) => {
|
||||
const diagnostics = [];
|
||||
const doc = context.state.doc.toString();
|
||||
@@ -175,50 +95,27 @@ const grantLinter = (context, schema, translate) => {
|
||||
}
|
||||
|
||||
// Parse key-value pairs separated by semicolons
|
||||
const pairs = trimmedLine.split(';').filter((p) => p.trim());
|
||||
const parsedFields = {};
|
||||
const fieldPositions = {};
|
||||
const allFields = [];
|
||||
const fieldKeyCount = {};
|
||||
const {
|
||||
fields: allFields,
|
||||
fieldMap: parsedFields,
|
||||
fieldPositions,
|
||||
fieldKeyCount,
|
||||
} = parseGrantLine(line, lineStart);
|
||||
|
||||
let pairOffset = trimStart; // Start from where trimmed content begins
|
||||
|
||||
for (const pair of pairs) {
|
||||
const pairInLine = line.indexOf(pair, pairOffset - trimStart);
|
||||
const [key, ...valueParts] = pair.split('=');
|
||||
const value = valueParts.join('='); // Handle cases where value contains '='
|
||||
|
||||
if (!key || value === undefined) {
|
||||
// Invalid format - not a key=value pair
|
||||
const pairStart = lineStart + pairInLine;
|
||||
// Check for invalid format (no '=' in a pair or empty key)
|
||||
for (const field of allFields) {
|
||||
if (field.value === undefined || !field.key) {
|
||||
diagnostics.push({
|
||||
from: pairStart,
|
||||
to: pairStart + pair.length,
|
||||
from: field.pos.keyStart,
|
||||
to: field.pos.valueEnd,
|
||||
severity: 'error',
|
||||
message: translate('general.invalid-format'),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const trimmedKey = key.trim();
|
||||
const keyStartInPair = pair.indexOf(trimmedKey);
|
||||
const valueStartInPair = pair.indexOf('=') + 1;
|
||||
|
||||
const pos = {
|
||||
keyStart: lineStart + pairInLine + keyStartInPair,
|
||||
valueStart: lineStart + pairInLine + valueStartInPair,
|
||||
valueEnd: lineStart + pairInLine + pair.length,
|
||||
};
|
||||
allFields.push({ key: trimmedKey, value: value.trim(), pos });
|
||||
fieldKeyCount[trimmedKey] = (fieldKeyCount[trimmedKey] ?? 0) + 1;
|
||||
|
||||
pairOffset = pairInLine + pair.length + 1; // +1 for semicolon
|
||||
}
|
||||
|
||||
// Validate there are no duplicate fields and store the last instance of each field for further validation
|
||||
// Validate there are no duplicate fields
|
||||
for (const field of allFields) {
|
||||
parsedFields[field.key] = field.value;
|
||||
fieldPositions[field.key] = field.pos;
|
||||
if (fieldKeyCount[field.key] > 1) {
|
||||
diagnostics.push({
|
||||
from: field.pos.keyStart,
|
||||
@@ -232,21 +129,20 @@ const grantLinter = (context, schema, translate) => {
|
||||
}
|
||||
|
||||
// Validate all fields are recognized
|
||||
for (const field of Object.keys(parsedFields)) {
|
||||
if (!GRANT_FIELDS.includes(field)) {
|
||||
const pos = fieldPositions[field];
|
||||
for (const field of allFields) {
|
||||
if (!GRANT_FIELDS.includes(field.key)) {
|
||||
diagnostics.push({
|
||||
from: pos.keyStart,
|
||||
to: pos.valueEnd,
|
||||
from: field.pos.keyStart,
|
||||
to: field.pos.valueEnd,
|
||||
severity: 'error',
|
||||
message: translate('fields.unknown-field', { field }),
|
||||
message: translate('fields.unknown-field', { field: field.key }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Validate field values are not empty
|
||||
for (const [field, value] of Object.entries(parsedFields)) {
|
||||
if (!value.trim()) {
|
||||
for (const field of Object.keys(fieldPositions)) {
|
||||
if (!parsedFields[field]) {
|
||||
const pos = fieldPositions[field];
|
||||
diagnostics.push({
|
||||
from: pos.valueStart,
|
||||
@@ -262,7 +158,7 @@ const grantLinter = (context, schema, translate) => {
|
||||
// Validate type field value is a valid resource type
|
||||
if (parsedFields.type) {
|
||||
validatedFields.type = { value: '', wildcard: false };
|
||||
const typeValue = parsedFields.type.trim();
|
||||
const typeValue = parsedFields.type;
|
||||
const pos = fieldPositions.type;
|
||||
if (!schema.validResourceTypes.includes(typeValue)) {
|
||||
diagnostics.push({
|
||||
@@ -273,7 +169,7 @@ const grantLinter = (context, schema, translate) => {
|
||||
});
|
||||
} else {
|
||||
validatedFields.type.value = typeValue;
|
||||
if (!parsedFields.ids) {
|
||||
if (!fieldPositions.ids) {
|
||||
if (!schema.topLevelTypes.includes(typeValue)) {
|
||||
diagnostics.push({
|
||||
from: pos.valueStart,
|
||||
@@ -317,7 +213,7 @@ const grantLinter = (context, schema, translate) => {
|
||||
});
|
||||
} else {
|
||||
validatedFields.ids.wildcard = true;
|
||||
if (!parsedFields.type) {
|
||||
if (!fieldPositions.type) {
|
||||
diagnostics.push({
|
||||
from: lineStart,
|
||||
to: lineEnd,
|
||||
@@ -343,14 +239,14 @@ const grantLinter = (context, schema, translate) => {
|
||||
}
|
||||
|
||||
if (!validatedFields.ids) {
|
||||
if (!validatedFields.type) {
|
||||
if (!fieldPositions.type) {
|
||||
diagnostics.push({
|
||||
from: lineStart,
|
||||
to: lineEnd,
|
||||
severity: 'error',
|
||||
message: translate('fields.missing-ids-or-type'),
|
||||
});
|
||||
} else if (validatedFields.type.wildcard) {
|
||||
} else if (validatedFields.type?.wildcard) {
|
||||
diagnostics.push({
|
||||
from: lineStart,
|
||||
to: lineEnd,
|
||||
@@ -358,7 +254,7 @@ const grantLinter = (context, schema, translate) => {
|
||||
message: translate('type.wildcard-requires-ids'),
|
||||
});
|
||||
} else {
|
||||
if (!parsedFields.actions && !parsedFields.output_fields) {
|
||||
if (!fieldPositions.actions && !fieldPositions.output_fields) {
|
||||
diagnostics.push({
|
||||
from: lineStart,
|
||||
to: lineEnd,
|
||||
@@ -368,7 +264,7 @@ const grantLinter = (context, schema, translate) => {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!parsedFields.actions && !parsedFields.output_fields) {
|
||||
if (!fieldPositions.actions && !fieldPositions.output_fields) {
|
||||
diagnostics.push({
|
||||
from: lineStart,
|
||||
to: lineEnd,
|
||||
@@ -489,30 +385,13 @@ const validateListField = (fieldValue, pos, diagnostics, translate) => {
|
||||
return valList;
|
||||
};
|
||||
|
||||
// Returns template, resource type, or null;
|
||||
// Returns 'template', resource type, or undefined
|
||||
const idType = (schema, id) => {
|
||||
if (id.startsWith('{{') && id.endsWith('}}')) {
|
||||
return 'template';
|
||||
}
|
||||
const prefix = id.split('_')[0];
|
||||
return schema.resourcesByPrefix[prefix];
|
||||
};
|
||||
|
||||
const getChildResourceActions = (schema, parentType) => {
|
||||
if (
|
||||
!parentType ||
|
||||
!schema.childResourceTypesByParentType[parentType]?.length
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const childTypes = schema.childResourceTypesByParentType[parentType] ?? [];
|
||||
|
||||
return [
|
||||
...new Set(
|
||||
childTypes.flatMap((type) => schema.resourcesByType[type]?.actions ?? []),
|
||||
),
|
||||
];
|
||||
return schema.resourceTypesByIdPrefix[prefix];
|
||||
};
|
||||
|
||||
const validateIdsField = (
|
||||
@@ -573,7 +452,7 @@ const validateIdsField = (
|
||||
}
|
||||
const currentIdType = idType(schema, id);
|
||||
if (currentIdType === 'template') {
|
||||
if (!ID_TEMPLATES.includes(id)) {
|
||||
if (!TEMPLATE_RESOURCE_TYPES[id]) {
|
||||
diagnostics.push({
|
||||
from: segmentStart,
|
||||
to: segmentEnd,
|
||||
@@ -652,29 +531,12 @@ const validateIdsField = (
|
||||
message: translate('ids.templates-cannot-combine'),
|
||||
});
|
||||
return;
|
||||
} else if (
|
||||
idsList[0] === '{{.Account.Id}}' ||
|
||||
idsList[0] === '{{account.id}}'
|
||||
) {
|
||||
seenIdType = 'account-template';
|
||||
} else if (idsList[0] === '{{.User.Id}}' || idsList[0] === '{{user.id}}') {
|
||||
seenIdType = 'user-template';
|
||||
}
|
||||
return TEMPLATE_RESOURCE_TYPES[idsList[0]];
|
||||
}
|
||||
return seenIdType;
|
||||
};
|
||||
|
||||
const getIdActionsForResourceType = (schema, resourceType) => {
|
||||
switch (resourceType) {
|
||||
case 'account-template':
|
||||
return ['*', ...schema.resourcesByType['account'].idActions];
|
||||
case 'user-template':
|
||||
return ['*', ...schema.resourcesByType['user'].idActions];
|
||||
default:
|
||||
return ['*', ...schema.resourcesByType[resourceType].idActions];
|
||||
}
|
||||
};
|
||||
|
||||
const validateActionsField = (
|
||||
schema,
|
||||
diagnostics,
|
||||
@@ -683,49 +545,34 @@ const validateActionsField = (
|
||||
pos,
|
||||
translate,
|
||||
) => {
|
||||
let validActions = schema.validActions;
|
||||
let validActions = ['*', 'no-op', ...schema.validActions];
|
||||
let errorMessage = (action) =>
|
||||
translate('actions.invalid-action', { action });
|
||||
if (!validatedFields.type) {
|
||||
if (validatedFields.ids?.knownType) {
|
||||
// Only id actions belonging to known id type (including wildcard action)
|
||||
validActions = getIdActionsForResourceType(
|
||||
schema,
|
||||
validatedFields.ids.knownType,
|
||||
);
|
||||
const { actionsType, actions } = getValidActions(schema, {
|
||||
typeValue: validatedFields.type?.value,
|
||||
idsValue: validatedFields.ids?.value,
|
||||
idsWildcard: validatedFields.ids?.wildcard,
|
||||
idsKnownType: validatedFields.ids?.knownType,
|
||||
});
|
||||
validActions = actions.length > 0 ? actions : validActions;
|
||||
|
||||
switch (actionsType) {
|
||||
case 'id':
|
||||
errorMessage = (action) =>
|
||||
translate('actions.id-only', {
|
||||
type: validatedFields.ids.knownType,
|
||||
action,
|
||||
});
|
||||
}
|
||||
} else if (validatedFields.type.wildcard) {
|
||||
if (validatedFields.ids?.knownType) {
|
||||
// Any action from associated child resource types would be valid (including wildcard action)
|
||||
validActions = [
|
||||
'*',
|
||||
'no-op',
|
||||
...getChildResourceActions(schema, validatedFields.ids.knownType),
|
||||
];
|
||||
}
|
||||
} else if (validatedFields.type.value) {
|
||||
if (!validatedFields.ids) {
|
||||
// Only collection actions create/list belonging to the specified type
|
||||
validActions =
|
||||
schema.resourcesByType[validatedFields.type.value].collectionActions;
|
||||
validActions = validActions.filter(
|
||||
(action) => action.startsWith('create') || action.startsWith('list'),
|
||||
);
|
||||
break;
|
||||
case 'collection':
|
||||
errorMessage = (action) =>
|
||||
translate('actions.collection-only', { action });
|
||||
} else if (validatedFields.ids.knownType || validatedFields.ids.wildcard) {
|
||||
// Any action belonging to the specified type (including wildcard action)
|
||||
validActions = [
|
||||
'*',
|
||||
'no-op',
|
||||
...schema.resourcesByType[validatedFields.type.value].actions,
|
||||
];
|
||||
}
|
||||
break;
|
||||
case 'child':
|
||||
case 'type':
|
||||
case 'all':
|
||||
validActions = ['no-op', ...validActions];
|
||||
break;
|
||||
}
|
||||
|
||||
let segmentStart = pos.valueStart;
|
||||
@@ -808,49 +655,22 @@ const validateOutputFieldsField = (
|
||||
pos,
|
||||
translate,
|
||||
) => {
|
||||
let validOutputFields = ['*'];
|
||||
let errorMessage = (field) =>
|
||||
translate('output-fields.invalid-field', { field });
|
||||
|
||||
if (validatedFields.type?.value && !validatedFields.type.wildcard) {
|
||||
const resourceOutputFields =
|
||||
schema.resourcesByType[validatedFields.type.value]?.outputFields ?? [];
|
||||
validOutputFields = ['*', ...resourceOutputFields];
|
||||
errorMessage = (field) =>
|
||||
translate('output-fields.invalid-field-for-type', {
|
||||
field,
|
||||
type: validatedFields.type.value,
|
||||
});
|
||||
} else if (validatedFields.type?.wildcard && validatedFields.ids?.knownType) {
|
||||
// Pinned IDs with wildcard type: validate against union of child types' output fields
|
||||
const parentType = validatedFields.ids.knownType;
|
||||
const childTypes = schema.childResourceTypesByParentType[parentType] ?? [];
|
||||
const childOutputFields = [
|
||||
...new Set(
|
||||
childTypes.flatMap(
|
||||
(type) => schema.resourcesByType[type]?.outputFields ?? [],
|
||||
),
|
||||
),
|
||||
];
|
||||
validOutputFields = ['*', ...childOutputFields];
|
||||
errorMessage = (field) =>
|
||||
translate('output-fields.invalid-field', { field });
|
||||
} else if (validatedFields.ids?.knownType && !validatedFields.type) {
|
||||
const knownType = validatedFields.ids.knownType;
|
||||
const TEMPLATE_RESOURCE_TYPES = {
|
||||
'account-template': 'account',
|
||||
'user-template': 'user',
|
||||
};
|
||||
const resolvedType = TEMPLATE_RESOURCE_TYPES[knownType] ?? knownType;
|
||||
const resourceOutputFields =
|
||||
schema.resourcesByType[resolvedType]?.outputFields ?? [];
|
||||
validOutputFields = ['*', ...resourceOutputFields];
|
||||
errorMessage = (field) =>
|
||||
translate('output-fields.invalid-field-for-type', {
|
||||
field,
|
||||
type: resolvedType,
|
||||
});
|
||||
}
|
||||
const {
|
||||
outputFieldsType,
|
||||
outputFields: validOutputFields,
|
||||
resolvedType,
|
||||
} = getValidOutputFields(schema, {
|
||||
typeValue: validatedFields.type?.value,
|
||||
idsKnownType: validatedFields.ids?.knownType,
|
||||
});
|
||||
const errorMessage =
|
||||
outputFieldsType === 'type'
|
||||
? (field) =>
|
||||
translate('output-fields.invalid-field-for-type', {
|
||||
field,
|
||||
type: resolvedType,
|
||||
})
|
||||
: (field) => translate('output-fields.invalid-field', { field });
|
||||
|
||||
let segmentStart = pos.valueStart;
|
||||
for (const [i, field] of outputFieldList.entries()) {
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
/**
|
||||
* Copyright IBM Corp. 2021, 2026
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export const GRANT_FIELDS = ['ids', 'type', 'actions', 'output_fields'];
|
||||
export const TEMPLATE_RESOURCE_TYPES = {
|
||||
'{{.User.Id}}': 'user',
|
||||
'{{.Account.Id}}': 'account',
|
||||
'{{user.id}}': 'user',
|
||||
'{{account.id}}': 'account',
|
||||
};
|
||||
|
||||
export const normalizeGrantsSchema = (grantsSchema) => {
|
||||
const resourceTypes = grantsSchema?.resource_types ?? [];
|
||||
|
||||
const resourcesByType = resourceTypes.reduce((resources, resourceType) => {
|
||||
const collectionActions = resourceType.collection_actions ?? [];
|
||||
const idActions = resourceType.id_actions ?? [];
|
||||
const idPrefixes = resourceType.id_prefixes ?? [];
|
||||
|
||||
resources[resourceType.type] = {
|
||||
actions: [...collectionActions, ...idActions],
|
||||
collectionActions,
|
||||
idActions,
|
||||
idPrefixes,
|
||||
parentType: resourceType.parent_type,
|
||||
outputFields: resourceType.output_fields ?? [],
|
||||
};
|
||||
|
||||
return resources;
|
||||
}, {});
|
||||
|
||||
const validResourceTypes = resourceTypes.map((resource) => resource.type);
|
||||
validResourceTypes.push('*');
|
||||
|
||||
const allActions = new Set();
|
||||
Object.values(resourcesByType).forEach((resource) => {
|
||||
resource.actions.forEach((action) => allActions.add(action));
|
||||
});
|
||||
const validActions = Array.from(allActions);
|
||||
|
||||
const resourceTypesByIdPrefix = resourceTypes.reduce((map, resource) => {
|
||||
resource.id_prefixes?.forEach((prefix) => {
|
||||
map[prefix] = resource.type;
|
||||
});
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
let parentTypes = new Set(
|
||||
resourceTypes.filter((r) => r.parent_type).map((r) => r.parent_type),
|
||||
);
|
||||
parentTypes = Array.from(parentTypes);
|
||||
|
||||
const topLevelTypes = resourceTypes
|
||||
.filter((r) => {
|
||||
const hasCreateAction = r.collection_actions.includes('create');
|
||||
const hasListAction = r.collection_actions.includes('list');
|
||||
return !r.parent_type && (hasCreateAction || hasListAction);
|
||||
})
|
||||
.map((r) => r.type);
|
||||
|
||||
const childResourceTypesByParentType = resourceTypes.reduce(
|
||||
(childrenByParentType, resourceType) => {
|
||||
const parentType = resourceType.parent_type;
|
||||
|
||||
if (!parentType) {
|
||||
return childrenByParentType;
|
||||
}
|
||||
|
||||
childrenByParentType[parentType] = [
|
||||
...(childrenByParentType[parentType] ?? []),
|
||||
resourceType.type,
|
||||
];
|
||||
|
||||
return childrenByParentType;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
return {
|
||||
resourcesByType,
|
||||
resourceTypesByIdPrefix,
|
||||
childResourceTypesByParentType,
|
||||
parentTypes,
|
||||
topLevelTypes,
|
||||
validResourceTypes,
|
||||
validActions,
|
||||
};
|
||||
};
|
||||
|
||||
export const parseGrantLine = (lineText = '', lineStart = 0) => {
|
||||
const pairs = lineText.split(';').filter((p) => p.trim());
|
||||
|
||||
const fields = [];
|
||||
const fieldMap = {};
|
||||
const fieldPositions = {};
|
||||
const fieldKeyCount = {};
|
||||
|
||||
let pairOffset = 0;
|
||||
|
||||
for (const pair of pairs) {
|
||||
const pairInLine = lineText.indexOf(pair, pairOffset);
|
||||
const [key, ...valueParts] = pair.split('=');
|
||||
const value = valueParts.join('=');
|
||||
|
||||
const trimmedKey = key.trim();
|
||||
const keyStartInPair = pair.indexOf(trimmedKey);
|
||||
const valueStartInPair = pair.indexOf('=') + 1;
|
||||
|
||||
const pos = {
|
||||
keyStart: lineStart + pairInLine + keyStartInPair,
|
||||
valueStart: lineStart + pairInLine + valueStartInPair,
|
||||
valueEnd: lineStart + pairInLine + pair.length,
|
||||
};
|
||||
|
||||
const hasValue = valueParts.length > 0;
|
||||
|
||||
fields.push({
|
||||
key: trimmedKey,
|
||||
value: hasValue ? value.trim() : undefined,
|
||||
pos,
|
||||
});
|
||||
fieldKeyCount[trimmedKey] = (fieldKeyCount[trimmedKey] ?? 0) + 1;
|
||||
|
||||
if (hasValue && trimmedKey) {
|
||||
fieldPositions[trimmedKey] = pos;
|
||||
const trimmedValue = value.trim();
|
||||
if (trimmedValue) {
|
||||
fieldMap[trimmedKey] = trimmedValue;
|
||||
}
|
||||
}
|
||||
|
||||
pairOffset = pairInLine + pair.length + 1;
|
||||
}
|
||||
|
||||
return { fields, fieldMap, fieldPositions, fieldKeyCount };
|
||||
};
|
||||
|
||||
const getChildResourceValues = (schema, compatibleType, getValues) => {
|
||||
if (
|
||||
!compatibleType ||
|
||||
!schema.childResourceTypesByParentType[compatibleType]?.length
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const childTypes =
|
||||
schema.childResourceTypesByParentType[compatibleType] ?? [];
|
||||
|
||||
return [
|
||||
...new Set(
|
||||
childTypes.flatMap((type) => getValues(schema.resourcesByType[type])),
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
const getChildResourceActions = (schema, compatibleType) =>
|
||||
getChildResourceValues(
|
||||
schema,
|
||||
compatibleType,
|
||||
(resource) => resource?.actions ?? [],
|
||||
);
|
||||
|
||||
const getChildResourceOutputFields = (schema, compatibleType) =>
|
||||
getChildResourceValues(
|
||||
schema,
|
||||
compatibleType,
|
||||
(resource) => resource?.outputFields ?? [],
|
||||
);
|
||||
|
||||
const withWildCard = (values = []) => ['*', ...new Set(values)];
|
||||
|
||||
export const getValidActions = (
|
||||
schema,
|
||||
{ typeValue, idsValue, idsWildcard, idsKnownType },
|
||||
) => {
|
||||
const typeWildcard = typeValue === '*';
|
||||
const hasExplicitType = Boolean(typeValue) && !typeWildcard;
|
||||
|
||||
if (idsValue) {
|
||||
// No type, ids resolve to a known type → that type's id actions.
|
||||
if (!typeValue && idsKnownType) {
|
||||
return {
|
||||
actionsType: 'id',
|
||||
actions: withWildCard(
|
||||
schema.resourcesByType[idsKnownType]?.idActions ?? [],
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// Wildcard type, known id type → child resource actions.
|
||||
if (typeWildcard && idsKnownType) {
|
||||
return {
|
||||
actionsType: 'child',
|
||||
actions: withWildCard(getChildResourceActions(schema, idsKnownType)),
|
||||
};
|
||||
}
|
||||
|
||||
// Explicit type → validate parent relationship then return type actions.
|
||||
if (hasExplicitType) {
|
||||
if (idsKnownType) {
|
||||
const selectedResource = schema.resourcesByType[typeValue];
|
||||
if (selectedResource?.parentType !== idsKnownType) {
|
||||
return { actionsType: 'invalid', actions: [] };
|
||||
}
|
||||
}
|
||||
return {
|
||||
actionsType: 'type',
|
||||
actions: withWildCard(schema.resourcesByType[typeValue]?.actions ?? []),
|
||||
};
|
||||
}
|
||||
|
||||
// Wildcard ids with no explicit type → all actions.
|
||||
if (idsWildcard && !hasExplicitType) {
|
||||
return { actionsType: 'all', actions: withWildCard(schema.validActions) };
|
||||
}
|
||||
} else {
|
||||
// No ids and no type → all actions.
|
||||
if (!typeValue) {
|
||||
return { actionsType: 'all', actions: withWildCard(schema.validActions) };
|
||||
}
|
||||
|
||||
// No ids, explicit top-level type → collection actions.
|
||||
if (hasExplicitType && schema.topLevelTypes.includes(typeValue)) {
|
||||
return {
|
||||
actionsType: 'collection',
|
||||
actions: schema.resourcesByType[typeValue]?.collectionActions ?? [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { actionsType: 'invalid', actions: [] };
|
||||
};
|
||||
|
||||
export const getValidOutputFields = (schema, { typeValue, idsKnownType }) => {
|
||||
const typeWildcard = typeValue === '*';
|
||||
const hasExplicitType = Boolean(typeValue) && !typeWildcard;
|
||||
|
||||
if (hasExplicitType) {
|
||||
const resourceOutputFields =
|
||||
schema.resourcesByType[typeValue]?.outputFields ?? [];
|
||||
return {
|
||||
outputFieldsType: 'type',
|
||||
outputFields: resourceOutputFields.length
|
||||
? withWildCard(resourceOutputFields)
|
||||
: ['*'],
|
||||
resolvedType: typeValue,
|
||||
};
|
||||
}
|
||||
|
||||
if (typeWildcard && idsKnownType) {
|
||||
const childOutputFields = getChildResourceOutputFields(
|
||||
schema,
|
||||
idsKnownType,
|
||||
);
|
||||
return {
|
||||
outputFieldsType: 'child',
|
||||
outputFields: childOutputFields.length
|
||||
? withWildCard(childOutputFields)
|
||||
: ['*'],
|
||||
resolvedType: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (!typeValue && idsKnownType) {
|
||||
const resourceOutputFields =
|
||||
schema.resourcesByType[idsKnownType]?.outputFields ?? [];
|
||||
return {
|
||||
outputFieldsType: 'type',
|
||||
outputFields: resourceOutputFields.length
|
||||
? withWildCard(resourceOutputFields)
|
||||
: ['*'],
|
||||
resolvedType: idsKnownType,
|
||||
};
|
||||
}
|
||||
|
||||
return { outputFieldsType: 'all', outputFields: ['*'], resolvedType: null };
|
||||
};
|
||||
@@ -72,6 +72,10 @@ module('Unit | Utility | grant-linter', function (hooks) {
|
||||
'type=credential;ids=csst_1234;actions=read,write;actions=delete',
|
||||
'Duplicate field "actions"',
|
||||
], // duplicate actions field
|
||||
[
|
||||
'type=credential;ids=cs_1234;actions=read;random=',
|
||||
'Unknown field "random"',
|
||||
], // unknown field "random"
|
||||
[
|
||||
'type=credential;ids=cs_1234;actions=read;random=name',
|
||||
'Unknown field "random"',
|
||||
@@ -80,6 +84,7 @@ module('Unit | Utility | grant-linter', function (hooks) {
|
||||
'=credential;actions=;ids=cs_1234',
|
||||
'Invalid format: expected key=value',
|
||||
], // missing type key
|
||||
['type;actions=;ids=cs_1234', 'Invalid format: expected key=value'], // missing type value and equal sign
|
||||
],
|
||||
function (assert, [grantString, errorMsg]) {
|
||||
const diagnostics = this.grantLinter(createContext(grantString));
|
||||
|
||||
Reference in new issue
Block a user