blob: 100dcb8c668516d24f3fea12b614e932b0aa965f [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
const HTTP_METHODS = new Set(['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace']);
function check(condition, message) {
if (!condition) {
throw new Error(message);
}
}
function decodePointerSegment(segment) {
return segment.replace(/~1/g, '/').replace(/~0/g, '~');
}
function validateCommon(fileName) {
const specPath = path.resolve(__dirname, '..', 'static', fileName);
const spec = yaml.load(fs.readFileSync(specPath, 'utf8'));
function checkSpec(condition, message) {
check(condition, `${fileName}: ${message}`);
}
function resolveLocalRef(ref) {
checkSpec(ref.startsWith('#/'), `Only local OpenAPI references are supported, found: ${ref}`);
return ref
.slice(2)
.split('/')
.map(decodePointerSegment)
.reduce((current, segment) => {
checkSpec(
current && Object.prototype.hasOwnProperty.call(current, segment),
`Unresolved OpenAPI reference: ${ref}`,
);
return current[segment];
}, spec);
}
function visit(value) {
if (Array.isArray(value)) {
value.forEach(visit);
return;
}
if (!value || typeof value !== 'object') {
return;
}
checkSpec(
!Object.prototype.hasOwnProperty.call(value, 'nullable'),
'OpenAPI 3.1 schemas must not use nullable',
);
if (typeof value.$ref === 'string') {
resolveLocalRef(value.$ref);
}
Object.values(value).forEach(visit);
}
function schema(name) {
const value = spec.components && spec.components.schemas && spec.components.schemas[name];
checkSpec(value, `Missing OpenAPI schema: ${name}`);
return value;
}
function requireProperties(schemaName, names) {
const properties = schema(schemaName).properties || {};
names.forEach((name) => {
checkSpec(properties[name], `Schema ${schemaName} is missing property: ${name}`);
});
return properties;
}
function requireRequiredProperties(schemaName, names) {
const required = schema(schemaName).required || [];
names.forEach((name) => {
checkSpec(required.includes(name), `Schema ${schemaName} must require property: ${name}`);
});
}
function requireTypedIntegerProperties(schemaName, names) {
const properties = requireProperties(schemaName, names);
names.forEach((name) => {
checkSpec(
properties[name].type === 'integer' && properties[name].format === 'int64',
`Schema ${schemaName}.${name} must be an int64 integer`,
);
});
}
function requireSchemaReference(schemaName, composition, referencedSchemaName) {
const references = schema(schemaName)[composition] || [];
const expected = `#/components/schemas/${referencedSchemaName}`;
checkSpec(
references.some((reference) => reference.$ref === expected),
`Schema ${schemaName}.${composition} is missing reference: ${expected}`,
);
}
function validatePathParameters(resourcePath, pathItem, operation) {
const templateNames = Array.from(resourcePath.matchAll(/\{([^}]+)\}/g), (match) => match[1]);
const parameters = [...(pathItem.parameters || []), ...(operation.parameters || [])].map(
(parameter) => (parameter.$ref ? resolveLocalRef(parameter.$ref) : parameter),
);
const pathParameters = parameters.filter((parameter) => parameter.in === 'path');
templateNames.forEach((name) => {
const parameter = pathParameters.find((candidate) => candidate.name === name);
checkSpec(parameter, `Path ${resourcePath} is missing path parameter: ${name}`);
checkSpec(
parameter.required === true,
`Path parameter ${resourcePath}.${name} must be required`,
);
});
pathParameters.forEach((parameter) => {
checkSpec(
templateNames.includes(parameter.name),
`Path ${resourcePath} declares unused path parameter: ${parameter.name}`,
);
});
}
checkSpec(spec.openapi === '3.1.1', `Expected OpenAPI 3.1.1, found: ${spec.openapi}`);
checkSpec(
spec.paths && spec.components && spec.components.schemas,
'Incomplete OpenAPI document',
);
visit(spec);
const operations = new Map();
for (const [resourcePath, pathItem] of Object.entries(spec.paths)) {
for (const [method, operation] of Object.entries(pathItem)) {
if (!HTTP_METHODS.has(method)) {
continue;
}
validatePathParameters(resourcePath, pathItem, operation);
checkSpec(
operation.operationId,
`Operation ${method.toUpperCase()} ${resourcePath} has no operationId`,
);
checkSpec(
!operations.has(operation.operationId),
`Duplicate operationId: ${operation.operationId}`,
);
operations.set(operation.operationId, operation);
}
}
function requireOperation(operationId) {
const operation = operations.get(operationId);
checkSpec(operation, `Missing operationId: ${operationId}`);
return operation;
}
function requireResponses(operationId, statusCodes) {
const responses = requireOperation(operationId).responses || {};
statusCodes.forEach((statusCode) => {
checkSpec(
responses[statusCode],
`Operation ${operationId} is missing response: ${statusCode}`,
);
});
}
return {
spec,
operations,
schema,
requireOperation,
requireProperties,
requireRequiredProperties,
requireTypedIntegerProperties,
requireSchemaReference,
requireResponses,
checkSpec,
};
}
function requireArrayOfIdentifiers(contract, schemaName, propertyName) {
const properties = contract.requireProperties(schemaName, [propertyName]);
contract.checkSpec(
properties[propertyName].type === 'array',
`Schema ${schemaName}.${propertyName} must be an array`,
);
contract.checkSpec(
properties[propertyName].items &&
properties[propertyName].items.$ref === '#/components/schemas/Identifier',
`Schema ${schemaName}.${propertyName} items must reference Identifier`,
);
}
function requireNullableStringProperty(contract, schemaName, propertyName) {
const property = contract.requireProperties(schemaName, [propertyName])[propertyName];
contract.checkSpec(
Array.isArray(property.type) &&
property.type.includes('string') &&
property.type.includes('null'),
`Schema ${schemaName}.${propertyName} must accept string and null`,
);
}
function requireExactEnum(contract, schemaName, expectedValues) {
const actualValues = contract.schema(schemaName).enum || [];
contract.checkSpec(
actualValues.length === expectedValues.length &&
expectedValues.every((value) => actualValues.includes(value)),
`Schema ${schemaName} must define enum values: ${expectedValues.join(', ')}`,
);
}
function validateCatalogOpenApi() {
const contract = validateCommon('rest-catalog-open-api.yaml');
[
'getConfig',
'createDatabase',
'getDatabase',
'alterDatabase',
'dropDatabase',
'createTable',
'getTable',
'alterTable',
'dropTable',
].forEach(contract.requireOperation);
const createOperation = contract.requireOperation('createPartitions');
const createRequestRef =
createOperation.requestBody.content['application/json'].schema.$ref;
contract.checkSpec(
createRequestRef === '#/components/schemas/CreatePartitionsRequest',
'createPartitions must use CreatePartitionsRequest',
);
contract.checkSpec(
!contract.operations.has('createPartitionsWithOptions'),
'partition options must use the existing createPartitions operation',
);
const createResponseRef =
createOperation.responses['200'].content['application/json'].schema.$ref;
contract.checkSpec(
createResponseRef === '#/components/schemas/CreatePartitionsResponse',
'createPartitions must use CreatePartitionsResponse',
);
contract.checkSpec(
createOperation.responses['400'] && createOperation.responses['501'],
'createPartitions must declare invalid and unsupported option responses',
);
const createRequestProperties = contract.requireProperties('CreatePartitionsRequest', [
'partitionSpecs',
'partitionOptions',
]);
const requestOptions = createRequestProperties.partitionOptions;
contract.checkSpec(
Array.isArray(requestOptions.type) &&
requestOptions.type.includes('array') &&
requestOptions.type.includes('null') &&
requestOptions.items.type === 'object' &&
requestOptions.items.additionalProperties.type === 'string',
'CreatePartitionsRequest.partitionOptions must be a nullable array of string maps',
);
contract.checkSpec(
requestOptions.description.includes('partitionSpecs') &&
requestOptions.description.toLowerCase().includes('position') &&
requestOptions.description.toLowerCase().includes('same length') &&
requestOptions.description.toLowerCase().includes('empty object'),
'CreatePartitionsRequest.partitionOptions must document positional alignment and empty options',
);
contract.requireProperties('Partition', ['options']);
contract.requireProperties('ConfigResponse', ['defaults', 'overrides']);
contract.requireProperties('CreateDatabaseRequest', ['name', 'options']);
contract.requireProperties('AlterDatabaseRequest', ['removals', 'updates']);
contract.requireProperties('CreateTableRequest', ['identifier', 'schema']);
contract.requireProperties('AlterTableRequest', ['changes']);
contract.requireProperties('Identifier', ['database', 'object']);
contract.requireProperties('Schema', [
'fields',
'partitionKeys',
'primaryKeys',
'options',
'comment',
]);
contract.requireProperties('DataField', ['id', 'name', 'type', 'description', 'defaultValue']);
contract.requireSchemaReference('DataType', 'oneOf', 'VectorType');
contract.requireProperties('VectorType', ['type', 'element', 'length']);
contract.requireSchemaReference('SchemaChange', 'anyOf', 'DropPrimaryKey');
contract.checkSpec(
contract.schema('BaseSchemaChange').discriminator.mapping.dropPrimaryKey ===
'#/components/schemas/DropPrimaryKey',
'BaseSchemaChange discriminator is missing dropPrimaryKey',
);
const dropPrimaryKey = contract.requireProperties('DropPrimaryKey', ['action']);
contract.checkSpec(
dropPrimaryKey.action.const === 'dropPrimaryKey',
'Schema DropPrimaryKey.action must be dropPrimaryKey',
);
contract.checkSpec(
contract.schema('BaseInstant').discriminator.propertyName === 'type',
'BaseInstant discriminator must use the JSON field type',
);
const updateViewComment = contract.requireProperties('UpdateViewComment', ['action', 'comment']);
contract.checkSpec(
!updateViewComment.key,
'Schema UpdateViewComment must use comment instead of key',
);
['UpdateComment', 'UpdateViewComment', 'UpdateFunctionComment'].forEach((schemaName) =>
requireNullableStringProperty(contract, schemaName, 'comment'),
);
const errorResourceTypes =
contract.requireProperties('ErrorResponse', ['resourceType']).resourceType.enum || [];
['FUNCTION', 'DEFINITION'].forEach((resourceType) => {
contract.checkSpec(
errorResourceTypes.includes(resourceType),
`Schema ErrorResponse.resourceType is missing value: ${resourceType}`,
);
});
requireArrayOfIdentifiers(contract, 'ListTablesGloballyResponse', 'tables');
requireArrayOfIdentifiers(contract, 'ListViewsGloballyResponse', 'views');
requireArrayOfIdentifiers(contract, 'ListFunctionsGloballyResponse', 'functions');
contract.requireProperties('ListFunctionsGloballyResponse', ['nextPageToken']);
const getFunctionProperties = contract.requireProperties('GetFunctionResponse', ['uuid']);
contract.checkSpec(
getFunctionProperties.uuid.type === 'string',
'Schema GetFunctionResponse.uuid must be a string',
);
['GetDatabaseResponse', 'GetTableResponse', 'GetViewResponse', 'GetFunctionResponse'].forEach(
(schemaName) => contract.requireTypedIntegerProperties(schemaName, ['createdAt', 'updatedAt']),
);
return contract.operations.size;
}
function validateManagementOpenApi() {
const contract = validateCommon('rest-management-open-api.yaml');
const operationIds = [
'listPermissions',
'grantPermission',
'revokePermission',
'listTablePolicies',
'createTablePolicy',
'dropTablePolicy',
];
const resourcePaths = [
'/v1/{prefix}/permissions',
'/v1/{prefix}/permissions/grant',
'/v1/{prefix}/permissions/revoke',
'/v1/{prefix}/databases/{database}/tables/{table}/policies',
'/v1/{prefix}/databases/{database}/tables/{table}/policies/drop',
];
resourcePaths.forEach((resourcePath) =>
contract.checkSpec(
contract.spec.paths[resourcePath],
`Missing management path: ${resourcePath}`,
),
);
[
'/v1/{prefix}/policies',
'/v1/{prefix}/databases/{database}/policies',
'/v1/{prefix}/databases/{database}/tables/{table}/policies/{policyName}',
].forEach((resourcePath) =>
contract.checkSpec(
!contract.spec.paths[resourcePath],
`Policies must not be attachable outside tables: ${resourcePath}`,
),
);
operationIds.forEach(contract.requireOperation);
['listPermissions', 'grantPermission', 'revokePermission'].forEach((operationId) =>
contract.requireResponses(operationId, [
'200',
'400',
'401',
'403',
'404',
'429',
'500',
'503',
]),
);
contract.requireResponses('grantPermission', ['409']);
['listTablePolicies', 'createTablePolicy'].forEach((operationId) =>
contract.requireResponses(operationId, [
'200',
'400',
'401',
'403',
'404',
'429',
'500',
'503',
]),
);
contract.requireResponses('dropTablePolicy', [
'200',
'400',
'401',
'403',
'404',
'429',
'500',
'503',
]);
contract.requireResponses('createTablePolicy', ['409']);
contract.checkSpec(
contract.spec.info.version === '1.0' &&
contract.spec.info.description.toLowerCase().includes('experimental'),
'The management contract must be versioned 1.0 and marked experimental',
);
contract.checkSpec(
!Object.prototype.hasOwnProperty.call(contract.spec, 'security'),
'The management contract must not require one deployment-specific authentication scheme',
);
const assignmentFields = ['resource', 'access', 'principal', 'columns', 'expireTime'];
contract.requireProperties('PermissionAssignment', assignmentFields);
contract.requireRequiredProperties('PermissionAssignment', ['resource', 'access', 'principal']);
const grantProperties = contract.requireProperties('GrantPermissionRequest', [
'resource',
'access',
'principal',
'columns',
'expireTime',
]);
contract.requireRequiredProperties('GrantPermissionRequest', [
'resource',
'access',
'principal',
]);
['policy', 'grantOption'].forEach((field) =>
contract.checkSpec(
!grantProperties[field],
`Schema GrantPermissionRequest must omit field: ${field}`,
),
);
const revokeProperties = contract.requireProperties('RevokePermissionRequest', [
'resource',
'access',
'principal',
]);
contract.requireRequiredProperties('RevokePermissionRequest', [
'resource',
'access',
'principal',
]);
['expireTime', 'columns', 'policy', 'grantOption'].forEach((field) =>
contract.checkSpec(
!revokeProperties[field],
`Schema RevokePermissionRequest must omit field: ${field}`,
),
);
const permissionList = contract.requireProperties('ListPermissionsResponse', [
'permissions',
'nextPageToken',
]);
contract.checkSpec(
permissionList.permissions.type === 'array' &&
permissionList.permissions.items.$ref === '#/components/schemas/PermissionAssignment',
'ListPermissionsResponse.permissions must contain PermissionAssignment values',
);
contract.requireRequiredProperties('ListPermissionsResponse', ['permissions']);
const principal = contract.schema('Principal');
contract.checkSpec(
principal.type === 'string' && principal.minLength === 1 && principal.maxLength === 128,
'Principal must be a non-empty string that fits the 128-character persistence identity',
);
contract.checkSpec(
!contract.spec.components.schemas.PrincipalRef &&
!contract.spec.components.schemas.PrincipalType &&
!contract.spec.components.parameters.PrincipalTypeQuery,
'Principal must not expose a separate reference object or type',
);
['PermissionAssignment', 'GrantPermissionRequest', 'RevokePermissionRequest'].forEach(
(schemaName) =>
contract.checkSpec(
contract.requireProperties(schemaName, ['principal']).principal.$ref ===
'#/components/schemas/Principal',
`Schema ${schemaName}.principal must reference Principal`,
),
);
contract.checkSpec(
contract.spec.components.parameters.PrincipalQuery.schema.$ref ===
'#/components/schemas/Principal',
'PrincipalQuery must reference Principal',
);
requireExactEnum(contract, 'ResourceType', [
'CATALOG',
'CATALOG_ALL',
'DATABASE',
'DATABASE_ALL',
'TABLE',
'COLUMN',
'FUNCTION',
'VIEW',
]);
requireExactEnum(contract, 'PolicyType', ['ROW_FILTER', 'COLUMN_MASKING']);
Object.entries({
CatalogResource: 'CATALOG',
CatalogAllResource: 'CATALOG_ALL',
DatabaseResource: 'DATABASE',
DatabaseAllResource: 'DATABASE_ALL',
TableResource: 'TABLE',
ColumnResource: 'COLUMN',
FunctionResource: 'FUNCTION',
ViewResource: 'VIEW',
RowFilterPolicyIdentity: 'ROW_FILTER',
ColumnMaskPolicyIdentity: 'COLUMN_MASKING',
TablePolicyResource: 'TABLE',
}).forEach(([schemaName, expectedType]) => {
const typeProperty = contract.requireProperties(schemaName, ['type']).type;
contract.checkSpec(
typeProperty.type === 'string' && typeProperty.const === expectedType,
`${schemaName}.type must be a typed string constant`,
);
});
const permissionAccess = contract.schema('PermissionAccess');
const expectedAccesses = [
'ALL',
'CREATEDATABASE',
'DESCRIBE',
'ALTER',
'DROP',
'CREATETABLE',
'CREATEFUNCTION',
'CREATEVIEW',
'LIST',
'SELECT',
'UPDATE',
'GRANT',
];
contract.checkSpec(
permissionAccess.type === 'string' &&
permissionAccess.enum.length === expectedAccesses.length &&
expectedAccesses.every((access) => permissionAccess.enum.includes(access)),
'PermissionAccess must define the complete data access enum',
);
contract.checkSpec(
permissionAccess.maxLength === 32,
'PermissionAccess must fit the 32-character persistence field',
);
['PermissionAssignment', 'GrantPermissionRequest', 'RevokePermissionRequest'].forEach(
(schemaName) =>
contract.checkSpec(
contract.requireProperties(schemaName, ['access']).access.$ref ===
'#/components/schemas/PermissionAccess',
`Schema ${schemaName}.access must reference PermissionAccess`,
),
);
contract.checkSpec(
contract.spec.components.parameters.AccessQuery.schema.$ref ===
'#/components/schemas/PermissionAccess',
'AccessQuery must reference PermissionAccess',
);
contract.requireSchemaReference('PermissionResource', 'oneOf', 'ColumnResource');
contract.requireSchemaReference('PermissionResource', 'oneOf', 'CatalogAllResource');
contract.requireSchemaReference('PermissionResource', 'oneOf', 'DatabaseAllResource');
const catalogAllResource = contract.requireProperties('CatalogAllResource', ['type']);
contract.requireRequiredProperties('CatalogAllResource', ['type']);
contract.checkSpec(
catalogAllResource.type.const === 'CATALOG_ALL',
'CatalogAllResource must use the CATALOG_ALL discriminator',
);
const databaseAllResource = contract.requireProperties('DatabaseAllResource', [
'type',
'database',
]);
contract.requireRequiredProperties('DatabaseAllResource', ['type', 'database']);
contract.checkSpec(
databaseAllResource.type.const === 'DATABASE_ALL',
'DatabaseAllResource must use the DATABASE_ALL discriminator',
);
const columnResource = contract.requireProperties('ColumnResource', [
'type',
'database',
'table',
]);
contract.requireRequiredProperties('ColumnResource', ['type', 'database', 'table']);
contract.checkSpec(
columnResource.type.const === 'COLUMN',
'ColumnResource must use the COLUMN discriminator',
);
const permissionColumns = contract.requireProperties('PermissionColumns', [
'columnNames',
'excludedColumnNames',
]);
const permissionColumnsDescription = contract
.schema('PermissionColumns')
.description.toLowerCase()
.replace(/\s+/g, ' ');
['intersected', 'fails the query', 'query authorization'].forEach((phrase) =>
contract.checkSpec(
permissionColumnsDescription.includes(phrase),
`PermissionColumns semantics are missing: ${phrase}`,
),
);
['columnNames', 'excludedColumnNames'].forEach((field) => {
const definition = permissionColumns[field];
contract.checkSpec(
definition.type === 'array' &&
definition.minItems === 1 &&
definition.uniqueItems === true &&
definition.items.type === 'string' &&
definition.items.minLength === 1,
`PermissionColumns.${field} must be a non-empty unique string array`,
);
});
const columnAlternatives = contract.schema('PermissionColumns').oneOf || [];
['columnNames', 'excludedColumnNames'].forEach((field) =>
contract.checkSpec(
columnAlternatives.some(
(alternative) =>
alternative.required &&
alternative.required.length === 1 &&
alternative.required[0] === field,
),
`PermissionColumns must define the ${field} alternative`,
),
);
['PermissionAssignment', 'GrantPermissionRequest'].forEach((schemaName) => {
const properties = contract.requireProperties(schemaName, ['columns']);
contract.checkSpec(
properties.columns.$ref === '#/components/schemas/PermissionColumns',
`${schemaName}.columns must reference PermissionColumns`,
);
contract.requireSchemaReference(schemaName, 'allOf', 'ColumnAssignmentConstraint');
});
['DatabaseQuery', 'TableQuery', 'FunctionQuery', 'ViewQuery'].forEach((parameterName) => {
const locatorQuery = contract.spec.components.parameters[parameterName];
contract.checkSpec(
locatorQuery.schema.type === 'string' && locatorQuery.schema.minLength === 1,
`${parameterName} must be a non-empty string`,
);
});
['TooManyRequests', 'ServiceUnavailable'].forEach((responseName) => {
const response = contract.spec.components.responses[responseName];
contract.checkSpec(response, `Missing reusable response: ${responseName}`);
contract.checkSpec(
response.headers['Retry-After'].$ref === '#/components/headers/RetryAfter',
`Response ${responseName} must expose the optional Retry-After header`,
);
});
contract.checkSpec(
contract.spec.components.headers.RetryAfter.schema.type === 'string',
'RetryAfter must allow HTTP delta-seconds or an HTTP date as a string',
);
const expireTime = contract.schema('ExpireTime');
contract.checkSpec(
expireTime.type === 'string' &&
expireTime.format === 'date-time' &&
expireTime.pattern === '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,3})?Z$',
'ExpireTime must use UTC Z with at most three fractional digits',
);
['PermissionAssignment', 'GrantPermissionRequest'].forEach((schemaName) => {
const expiry = contract.requireProperties(schemaName, ['expireTime']).expireTime;
contract.checkSpec(
expiry.$ref === '#/components/schemas/ExpireTime',
`${schemaName}.expireTime must reference the shared ExpireTime schema`,
);
});
const expireTimePattern = new RegExp(expireTime.pattern);
['2027-01-01T00:00:00Z', '2027-01-01T00:00:00.1Z', '2027-01-01T00:00:00.123Z'].forEach(
(value) =>
contract.checkSpec(expireTimePattern.test(value), `ExpireTime must accept ${value}`),
);
['2027-01-01T00:00:00.123456Z', '2027-01-01T00:00:00+00:00'].forEach((value) =>
contract.checkSpec(!expireTimePattern.test(value), `ExpireTime must reject ${value}`),
);
const expiryDescription = expireTime.description.toLowerCase();
contract.checkSpec(
expiryDescription.includes('exclusive') &&
expiryDescription.includes('server clock') &&
expiryDescription.includes('millisecond') &&
expiryDescription.includes('must not authorize'),
'expireTime must define exclusive millisecond server-clock authorization semantics',
);
const resourceDescription = contract.schema('PermissionResource').description.toLowerCase();
['stable internal resource identity', 'renaming', 'dropping', 'recreating'].forEach((phrase) =>
contract.checkSpec(
resourceDescription.includes(phrase),
`PermissionResource lifecycle is missing: ${phrase}`,
),
);
const policyDescription = contract.schema('DataPolicy').description.toLowerCase();
['logical and', 'same column', 'fail closed'].forEach((phrase) =>
contract.checkSpec(
policyDescription.includes(phrase),
`DataPolicy composition is missing: ${phrase}`,
),
);
contract.requireSchemaReference('PolicyRequest', 'oneOf', 'RowFilterPolicyRequest');
contract.requireSchemaReference('PolicyRequest', 'oneOf', 'ColumnMaskPolicyRequest');
contract.requireSchemaReference('DataPolicy', 'oneOf', 'RowFilterDataPolicy');
contract.requireSchemaReference('DataPolicy', 'oneOf', 'ColumnMaskDataPolicy');
const rowFilter = contract.requireProperties('RowFilter', ['predicate']);
const columnMask = contract.requireProperties('ColumnMask', ['onColumn', 'transform']);
contract.requireRequiredProperties('RowFilter', ['predicate']);
contract.requireRequiredProperties('ColumnMask', ['onColumn', 'transform']);
[rowFilter.predicate, columnMask.transform].forEach((definition) => {
contract.checkSpec(
definition.type === 'string' &&
definition.minLength === 1 &&
definition['x-maxUtf8Bytes'] === 61440 &&
definition.contentMediaType === 'application/json',
'Policy definitions must be bounded non-empty JSON strings',
);
});
['RowFilterPolicyRequest', 'ColumnMaskPolicyRequest'].forEach((schemaName) => {
const properties = contract.requireProperties(schemaName, ['principal']);
contract.checkSpec(
properties.principal.$ref === '#/components/schemas/Principal',
`${schemaName}.principal must reference Principal`,
);
contract.checkSpec(
!properties.type &&
!properties.resource &&
!properties.name &&
!properties.toPrincipals &&
!properties.exceptPrincipals,
`${schemaName} must expose only one principal and no path identity`,
);
contract.requireRequiredProperties(schemaName, ['principal']);
});
contract.requireProperties('RowFilterPolicyRequest', ['rowFilter']);
contract.requireProperties('ColumnMaskPolicyRequest', ['columnMask']);
contract.requireRequiredProperties('RowFilterPolicyRequest', ['rowFilter']);
contract.requireRequiredProperties('ColumnMaskPolicyRequest', ['columnMask']);
contract.requireSchemaReference('DropPolicyRequest', 'oneOf', 'RowFilterPolicyIdentity');
contract.requireSchemaReference('DropPolicyRequest', 'oneOf', 'ColumnMaskPolicyIdentity');
contract.requireRequiredProperties('RowFilterPolicyIdentity', ['type', 'principal']);
contract.requireRequiredProperties('ColumnMaskPolicyIdentity', [
'type',
'principal',
'column',
]);
const tablePolicyResource = contract.requireProperties('TablePolicyResource', [
'type',
'database',
'table',
]);
contract.checkSpec(
tablePolicyResource.type.const === 'TABLE',
'Data policies must use a TABLE attachment resource',
);
const policyList = contract.requireProperties('ListPoliciesResponse', [
'policies',
'nextPageToken',
]);
contract.checkSpec(
policyList.policies.items.$ref === '#/components/schemas/DataPolicy',
'ListPoliciesResponse.policies must contain DataPolicy values',
);
['RowFilterDataPolicy', 'ColumnMaskDataPolicy'].forEach((schemaName) => {
const properties = contract.requireProperties(schemaName, ['resource', 'principal']);
contract.checkSpec(
properties.principal.$ref === '#/components/schemas/Principal',
`${schemaName}.principal must reference Principal`,
);
contract.checkSpec(
!properties.name && !properties.toPrincipals && !properties.exceptPrincipals,
`${schemaName} must use a single principal identity`,
);
});
contract.requireProperties('ErrorResponse', [
'message',
'resourceType',
'resourceName',
'code',
]);
contract.requireRequiredProperties('ErrorResponse', ['message', 'code']);
const permissionParameters = contract
.requireOperation('listPermissions')
.parameters.map((parameter) =>
parameter.$ref ? parameter.$ref.split('/').pop() : parameter.name,
);
[
'ResourceTypeQuery',
'DatabaseQuery',
'TableQuery',
'FunctionQuery',
'ViewQuery',
'PrincipalQuery',
'AccessQuery',
'PageToken',
'MaxResults',
].forEach((name) =>
contract.checkSpec(
permissionParameters.includes(name),
`Operation listPermissions is missing query parameter: ${name}`,
),
);
contract.checkSpec(
permissionParameters.length === 9,
'Operation listPermissions must expose only exact-resource filters and pagination',
);
contract.checkSpec(
contract.spec.components.parameters.ResourceTypeQuery.required === true,
'Operation listPermissions must require resourceType',
);
const policyParameters = contract
.requireOperation('listTablePolicies')
.parameters.map((parameter) =>
parameter.$ref ? parameter.$ref.split('/').pop() : parameter.name,
);
['PolicyTypeQuery', 'PrincipalQuery', 'PolicyColumnQuery', 'PageToken', 'MaxResults'].forEach(
(name) =>
contract.checkSpec(
policyParameters.includes(name),
`Operation listTablePolicies is missing query parameter: ${name}`,
),
);
contract.checkSpec(
!policyParameters.includes('PolicyNameQuery'),
'Principal-scoped policies must not expose a policy-name filter',
);
const tablePoliciesPath =
contract.spec.paths['/v1/{prefix}/databases/{database}/tables/{table}/policies'];
const dropTablePolicyPath =
contract.spec.paths['/v1/{prefix}/databases/{database}/tables/{table}/policies/drop'];
contract.checkSpec(
tablePoliciesPath.get &&
tablePoliciesPath.post &&
!tablePoliciesPath.put &&
!tablePoliciesPath.delete,
'The table policy collection must expose only list and strict creation',
);
contract.checkSpec(
dropTablePolicyPath.post && !dropTablePolicyPath.delete,
'Policy deletion must use a body-bearing POST action instead of DELETE',
);
contract.checkSpec(
contract.operations.size === operationIds.length,
`The management contract must define exactly ${operationIds.length} operations`,
);
resourcePaths.forEach((resourcePath) =>
contract.checkSpec(
contract.spec.paths[resourcePath].parameters.some(
(parameter) => parameter.$ref === '#/components/parameters/Prefix',
),
`Path ${resourcePath} must reuse components.parameters.Prefix`,
),
);
return contract.operations.size;
}
const catalogOperationCount = validateCatalogOpenApi();
const managementOperationCount = validateManagementOpenApi();
console.log(`Validated REST Catalog OpenAPI contract with ${catalogOperationCount} operations.`);
console.log(
`Validated REST Management OpenAPI contract with ${managementOperationCount} operations.`,
);