@oada/oadaify
Version:
Make OADA data nicer to work with in JS/TS
227 lines (196 loc) • 5.77 kB
text/typescript
/**
* @license
* Copyright 2022 Alex Layton
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
// This is better than Omit
import type { Except } from 'type-fest';
// TS is dumb about symbol keys
/**
* Symbol to access OADA `_id` key
*/
export const _id: unique symbol = Symbol('_id');
/**
* Symbol to access OADA `_rev` key
*/
export const _rev: unique symbol = Symbol('_rev');
/**
* Symbol to access OADA `_type` key
*/
export const _type: unique symbol = Symbol('_type');
/**
* Symbol to access OADA `_meta` key
*/
export const _meta: unique symbol = Symbol('_meta');
/**
* @todo just declare symbols in here if TS stops being dumb about symbol keys
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
const Symbols = <const>{
_id,
_rev,
_type,
_meta,
};
// Yes these are defined in type-fest, but mine are slightly different...
export type JsonObject = { [Key in string]?: JsonValue };
export type JsonArray = JsonValue[] | readonly JsonValue[];
export type JsonValue =
| string
| number
| boolean
// eslint-disable-next-line @typescript-eslint/ban-types
| null
| JsonObject
| JsonArray;
export type OADAified<T> = T extends JsonValue ? OADAifiedJsonValue<T> : T;
type OADAifyKey<T, K> = K extends keyof T ? T[K] : never;
/**
* @todo Better name
*/
export type OADAifiedJsonObject<T extends JsonObject = JsonObject> = {
[_id]: OADAifyKey<T, '_id'>;
[_rev]: OADAifyKey<T, '_rev'>;
[_type]: OADAifyKey<T, '_type'>;
/**
* @todo OADAify under _meta or not?
*/
[_meta]: OADAified<T['_meta']>;
} & {
[K in keyof Except<T, keyof typeof Symbols>]: OADAified<T[K]>;
};
/**
* @todo Better name
*/
export type OADAifiedJsonArray<T extends JsonArray = JsonArray> = Array<
OADAifiedJsonValue<
T extends Array<infer R> ? R : T extends ReadonlyArray<infer R> ? R : never
>
>;
/**
* @todo Better name
*/
export type OADAifiedJsonValue<T extends JsonValue = JsonValue> =
T extends JsonArray
? OADAifiedJsonArray<T>
: T extends JsonObject
? OADAifiedJsonObject<T>
: T;
/**
* @todo why doesn't TS figure this correctly with for ... in?
*/
export type StringKey<T extends JsonObject> = keyof T & string;
function isArray(value: unknown): value is unknown[] | readonly unknown[] {
return Array.isArray(value);
}
/**
* Converts OADA keys (i.e., ones starting with `_`) to Symbols
*
* This way when looping etc. you only get actual data keys.
* _Should_ turn itself back to original JSON for stringify, ajv, etc.
*/
export function oadaify<T extends JsonValue>(
value: T,
deep?: boolean
): OADAified<T>;
export function oadaify(value: JsonValue, deep = true): OADAifiedJsonValue {
if (!value || typeof value !== 'object') {
// Nothing to OADAify
return value;
}
if (isArray(value)) {
// Map ourself over arrays
return deep
? value.map((v) => oadaify(v)!)
: (Array.from(value) as OADAifiedJsonArray);
}
const out: Partial<OADAifiedJsonObject> = deep
? Object.fromEntries(
Object.entries(value).map(([k, v]) => [k, oadaify(v!)!])
)
: ({ ...value } as unknown as OADAifiedJsonObject);
// OADAify any OADA keys
// Have to explicitly handle each symbol for TS to understand...
if ('_id' in value) {
// eslint-disable-next-line security/detect-object-injection
out[_id] = `${value._id}`;
Object.defineProperty(out, '_id', {
value: value._id,
enumerable: false,
});
}
if ('_rev' in value) {
// eslint-disable-next-line security/detect-object-injection
out[_rev] = Number(value._rev);
Object.defineProperty(out, '_rev', {
value: value._rev,
enumerable: false,
});
}
if ('_type' in value) {
// eslint-disable-next-line security/detect-object-injection
out[_type] = `${value._type}`;
Object.defineProperty(out, '_type', {
value: value._type,
enumerable: false,
});
}
// TODO: Should _meta be OADAified?
if ('_meta' in value) {
// eslint-disable-next-line security/detect-object-injection
out[_meta] = value._meta as OADAifiedJsonValue;
Object.defineProperty(out, '_meta', {
value: value._meta,
enumerable: false,
});
}
// Make the JSON still right
Object.defineProperty(out, 'toJSON', { enumerable: false, value: toJSON });
return out as OADAifiedJsonObject;
}
/**
* Inverse of oadaify
*
* Makes OADA keys normal object properties again.
*
* @see oadaify
*/
export function deoadaify<T extends JsonValue>(value: OADAified<T>): T {
if (!value || typeof value !== 'object') {
return value as T;
}
if (Array.isArray(value)) {
return value.map((v) => deoadaify<JsonValue>(v)) as unknown as T;
}
const out = Object.fromEntries(
Object.entries(value).map(([k, v]) => [k, deoadaify<JsonValue>(v!)])
);
// Add OADA keys
if (Object.prototype.hasOwnProperty.call(value, _id)) {
// eslint-disable-next-line security/detect-object-injection
out._id = value[_id]!;
}
if (Object.prototype.hasOwnProperty.call(value, _rev)) {
// eslint-disable-next-line security/detect-object-injection
out._rev = value[_rev]!;
}
if (Object.prototype.hasOwnProperty.call(value, _type)) {
// eslint-disable-next-line security/detect-object-injection
out._type = value[_type]!;
}
if (Object.prototype.hasOwnProperty.call(value, _meta)) {
// eslint-disable-next-line security/detect-object-injection
out._meta = value[_meta]!;
}
return out as T;
}
/**
* Makes the OADA keys reappear for JSON.stringify
*/
function toJSON(this: OADAifiedJsonObject) {
return deoadaify(this);
}
export default oadaify;