UNPKG

@nerdware/ddb-single-table

Version:

A schema-based DynamoDB modeling tool, high-level API, and type-generator built to supercharge single-table designs!⚡

203 lines (202 loc) 11.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TableKeysSchema = void 0; const ts_type_safety_utils_1 = require("@nerdware/ts-type-safety-utils"); const errors_js_1 = require("../utils/errors.js"); const BaseSchema_js_1 = require("./BaseSchema.js"); /** * This class and its `BaseSchema` parent currently only serve to organize schema-related types, * validation methods, etc., but may be used to create schema instances in the future. This is * currently not the case, as schema attributes would need to be nested under an instance property * (e.g. `this.attributes`), which would require a lot of refactoring. If/when this is implemented, * schema instances would also be given "metadata" props like "name", "version", "schemaType", etc. */ class TableKeysSchema extends BaseSchema_js_1.BaseSchema { /** * This function validates the provided `tableKeysSchema`, and if valid, returns a * {@link TableKeysAndIndexes} object specifying the `tableHashKey`, `tableRangeKey`, * and a map of any included `indexes`. * * This function performs the following validation checks: * * 1. Ensure all key/index attributes specify `isHashKey`, `isRangeKey`, or `index`. * 2. Ensure exactly 1 table hash key, and 1 table range key are specified. * 3. Ensure all key/index attribute `type`s are "string", "number", or "Buffer" (S/N/B in the DDB API). * 4. Ensure all key/index attributes are `required`. * 5. Ensure there are no duplicate index names. * * @param tableKeysSchema - The schema to validate. * @param name - The `name` specified in the {@link SchemaMetadata|schema's metadata}. * @returns A {@link TableKeysAndIndexes} object. * @throws {SchemaValidationError} if the provided TableKeysSchema is invalid. */ static validate = (tableKeysSchema, { name: schemaName = "TableKeysSchema" } = {}) => { // First run the base Schema validation checks: BaseSchema_js_1.BaseSchema.validateAttributeTypes(tableKeysSchema, { schemaType: "TableKeysSchema", name: schemaName, }); // Then perform TableKeysSchema-specific validation checks: let tableHashKey; let tableRangeKey; let indexes; for (const keyAttrName in tableKeysSchema) { const { isHashKey, isRangeKey, index, type, required } = tableKeysSchema[keyAttrName]; // Ensure all key/index attributes specify `isHashKey`, `isRangeKey`, or `index` if (isHashKey !== true && isRangeKey !== true && index === undefined) { throw new errors_js_1.SchemaValidationError({ schemaName, problem: `attribute "${keyAttrName}" is not configured as a key or index`, }); } // Ensure all key/index attribute `type`s are "string", "number", or "Buffer" (S/N/B in DDB) if (!["string", "number", "Buffer"].includes(type)) { throw new errors_js_1.SchemaValidationError({ schemaName, problem: `attribute "${keyAttrName}" has an invalid "type" (must be "string", "number", or "Buffer")`, }); } // Ensure all key/index attributes are `required` if (required !== true) { throw new errors_js_1.SchemaValidationError({ schemaName, problem: `attribute "${keyAttrName}" is not "required"`, }); } // Check for table hashKey if (isHashKey === true) { // Throw error if tableHashKey is already defined if (tableHashKey) { throw new errors_js_1.SchemaValidationError({ schemaName, problem: `multiple table hash keys ("${tableHashKey}" and "${keyAttrName}")`, }); } tableHashKey = keyAttrName; } // Check for table rangeKey if (isRangeKey === true) { // Throw error if tableRangeKey is already defined if (tableRangeKey) { throw new errors_js_1.SchemaValidationError({ schemaName, problem: `multiple table range keys ("${tableRangeKey}" and "${keyAttrName}")`, }); } tableRangeKey = keyAttrName; } // Check for index if (index) { // Ensure index has a name if (!index.name) { throw new errors_js_1.SchemaValidationError({ schemaName, problem: `the index for attribute "${keyAttrName}" is missing a "name"`, }); } // See if "indexes" has been defined yet if (!indexes) { // If accum does not yet have "indexes", add it. indexes = {}; // Else ensure the index name is unique } else if ((0, ts_type_safety_utils_1.hasKey)(indexes, index.name)) { throw new errors_js_1.SchemaValidationError({ schemaName, problem: `multiple indexes with the same name ("${index.name}")`, }); } indexes[index.name] = { name: index.name, type: index.global === true ? "GLOBAL" : "LOCAL", indexPK: keyAttrName, ...(index.rangeKey && { indexSK: index.rangeKey }), }; } } // Ensure table hashKey exists if (!tableHashKey) { throw new errors_js_1.SchemaValidationError({ schemaName, problem: `the schema does not contain a hash key (must specify exactly one attribute with "isHashKey: true")`, }); } return { tableHashKey, ...(tableRangeKey && { tableRangeKey }), ...(indexes && { indexes }), }; }; /** * This function returns a ModelSchema with attributes and attribute-configs merged in from the * provided TableKeysSchema, thereby preventing the need to repeat key/index attribute configs in * every ModelSchema. Note that when using this "Partial ModelSchema" approach, the schema object * provided to the `ItemTypeFromSchema` generic must be the "complete" ModelSchema returned from * this function to ensure correct item typing. * * For ModelSchema in which it's desirable to include key/index attributes (e.g., to define a * Model-specific `"alias"`), please note the table below that oulines which attribute-configs * may be included and/or customized. * * | Key/Index Attribute Config | Can Include in ModelSchema | Can Customize in ModelSchema | * | :------------------------- | :------------------------: | :--------------------------: | * | `alias` | ✅ | ✅ | * | `default` | ✅ | ✅ | * | `transformValue` | ✅ | ✅ | * | `validate` | ✅ | ✅ | * | `type` | ✅ | ❌ | * | `required` | ✅ | ❌ | * | `isHashKey` | ❌ | ❌ | * | `isRangeKey` | ❌ | ❌ | * | `index` | ❌ | ❌ | * * > Note: `schema` and `oneOf` are not included in the table above, as they are not valid for * key/index attributes which must be of type "string", "number", or "Buffer". * * @param tableKeysSchema - The TableKeysSchema to merge into the ModelSchema. * @param modelSchema - The ModelSchema to merge the TableKeysSchema into. * @throws {SchemaValidationError} if the provided ModelSchema contains invalid key/index attribute configs. */ static getMergedModelSchema = ({ tableKeysSchema, modelSchema, }) => { const mergedModelSchema = { ...modelSchema }; for (const keyAttrName in tableKeysSchema) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { isHashKey, isRangeKey, index, ...keyAttrConfig } = tableKeysSchema[keyAttrName]; // Check if ModelSchema contains keyAttrName if ((0, ts_type_safety_utils_1.hasKey)(modelSchema, keyAttrName)) { // If ModelSchema contains keyAttrName, check if it contains mergeable config properties. ["type", "required"].forEach((attrConfigName) => { if ((0, ts_type_safety_utils_1.hasKey)(modelSchema[keyAttrName], attrConfigName)) { // If ModelSchema contains `keyAttrName` AND a mergeable property, ensure it matches TableKeysSchema. if (modelSchema[keyAttrName][attrConfigName] !== keyAttrConfig[attrConfigName]) { // Throw error if ModelSchema key attrConfig has a config mismatch throw new errors_js_1.SchemaValidationError({ schemaName: "ModelSchema", problem: `the "${attrConfigName}" config in the ModelSchema for key attribute "${keyAttrName}" does not match the TableKeysSchema`, }); } } else { // If ModelSchema contains `keyAttrName`, but NOT a mergeable config property, add it. mergedModelSchema[keyAttrName][attrConfigName] = keyAttrConfig[attrConfigName]; } }); } else { // If ModelSchema does NOT contain keyAttrName, add it. mergedModelSchema[keyAttrName] = keyAttrConfig; } } // Ensure the returned schema doesn't contain configs which are only valid in the TableKeysSchema. for (const attrName in mergedModelSchema) { ["isHashKey", "isRangeKey", "index"].forEach((attrConfigName) => { if ((0, ts_type_safety_utils_1.hasKey)(mergedModelSchema[attrName], attrConfigName)) { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete mergedModelSchema[attrName][attrConfigName]; } }); } return mergedModelSchema; }; } exports.TableKeysSchema = TableKeysSchema;