UNPKG

rajt

Version:

A serverless bundler layer, fully typed for AWS Lambda (Node.js and LLRT) and Cloudflare Workers.

206 lines (161 loc) 5.46 kB
import type { SchemaStructure } from './types' import getLength from '../utils/lenght' import { isArraySchema } from './schema' export default class Compact { static #typeRegex: RegExp static #reverseTypeRegex: RegExp static #reverseTypeMap: Record<string, string> static #typeMap: Record<string, string> = { // Boolean 'true': 'T', 'false': 'F', // Null 'null': 'N', // Array '[]': 'A', '["0"]': 'A0', '["1"]': 'A1', '["false"]': 'A2', '["true"]': 'A3', // Object '{}': 'O', // String '""': 'S', '"0"': 'S0', '"1"': 'S1', '"false"': 'S2', '"true"': 'S3', } static { const keys = [] const values = [] const reverseTypeMap: Record<string, string> = {} for (const key in this.#typeMap) { const val = this.#typeMap[key] const k = key.replace(/"/g, "'") keys.push(k) values.push(val) reverseTypeMap[val] = k this.#typeMap[k] = val } this.#reverseTypeMap = reverseTypeMap this.#typeRegex = this.#mapRegex(keys) this.#reverseTypeRegex = this.#mapRegex(values) } static encode(obj: any, schema: SchemaStructure): string { const seen: any[] = [] return this.#pack( JSON.stringify(this.zip(obj, schema, seen)) .replace(/"\^(\d+)"/g, '^$1') .replace(/"/g, '~TDQ~') .replace(/'/g, '"') .replace(/~TDQ~/g, "'") .replace(/\\'/g, "^'") ) } static smartDecode<T = any>(val: any, schema: SchemaStructure): T { if (!val) return val as T if (Array.isArray(val)) // @ts-ignore return val.map((i: {v: string}) => this.decode<T>(i?.V, schema)).filter(Boolean) as T return val?.V ? this.decode<T>(val.V, schema) : val } static decode<T = any>(val: string, schema: SchemaStructure): T { if (!val || typeof val !== 'string') return val as T return this.withSchema(this.unzip(JSON.parse( this.#unpack(val) .replace(/"/g, '~TSQ~') .replace(/'/g, '"') .replace(/~TSQ~/g, "'") .replace(/\^"/g, '\\"') .replace(/(?<=[,{\[]|^)(\^\d+)(?=[,\]}[]|$)/g, '"$1"') )), schema) as T } static zip(obj: any, schema: SchemaStructure, seen: any[]): any[] { if (Array.isArray(obj)) return obj?.length ? obj.map(item => this.zip(item, schema, seen)) : [] if (this.#cantZip(obj)) return obj return schema.map(key => { if (typeof key === 'string') return this.memo(obj[key], seen) const mainKey = Object.keys(key)[0] const subKeys = key[mainKey] const val = obj[mainKey] if (Array.isArray(val)) return val.map(item => this.zip(item, subKeys, seen)) return this.zip(val, subKeys, seen) }) } static unzip(val: any, seen: any[] = []): any[] { if (Array.isArray(val)) return val?.length ? val.map(item => this.unzip(item, seen)) : [] const type = typeof val const length = getLength(val, type) if (this.#cantZip(val, type, length)) return val if (type == 'object') { for (const key in val) val[key] = this.unzip(val[key], seen) return val } if (type == 'string' && val.startsWith('^')) { const item = seen[parseInt(val.slice(1), 10)] return item ? item : val } seen.push(val) return val } static withSchema(value: any[], keys: any[], deep = false): any { if (!value || !Array.isArray(value)) return value if (!deep && isArraySchema(keys)) return value?.length ? value.map(v => this.withSchema(v, keys, true)) : [] return Object.fromEntries( keys.map((key, index) => this.entry(key, value[index])).filter(Boolean) ) } static entry(key: any, value: any): any { if (!key) return undefined if (typeof key == 'string') return [key, value === undefined ? null : value] const mainKey = Object.keys(key)[0] const subKeys = key[mainKey] if (Array.isArray(value)) { if (value.length < 1) return [mainKey, []] return Array.isArray(value[0]) ? [mainKey, value.map(v => this.withSchema(v, subKeys))] : [mainKey, this.withSchema(value, subKeys)] } return [mainKey, value === undefined ? null : value] } static memo(val: any, seen: any[]): any { if (Array.isArray(val)) return val.map(item => this.memo(item, seen)) const type = typeof val if (type == 'object' && val != null) { for (const key in val) val[key] = this.memo(val[key], seen) return val } const length = getLength(val, type) if (this.#cantZip(val, type, length)) return val const index = seen.indexOf(val) if (index !== -1) return `^${index}` seen.push(val) return val } static #mapRegex(keys: string[]) { keys = keys.sort((a, b) => b.length - a.length).map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) return new RegExp(`(?<![^\\s,\\[\\{:])(${keys.join('|')})(?![^\\s,\\]\\}:])`, 'g') } static #pack(val: string): string { return val.replace(this.#typeRegex, match => this.#typeMap[match]) } static #unpack(val: string): string { return val.replace(this.#reverseTypeRegex, match => this.#reverseTypeMap[match]) } static #cantZip(val: any, type: string = '', length: number = 0) { if (!val || [null, true, false, 'true', 'false'].includes(val)) return true return !type && !length ? false : type != 'object' && length < 2 } }