@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
JavaScript
;
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;