@platform/cell.schema
Version:
URI and database schemas for the `cell.os`.
167 lines (166 loc) • 6.55 kB
JavaScript
import { cuid, slug, coord, wildcard } from '../common';
const ALLOW = { NS: [] };
export const DEFAULT = { ALLOW };
export class Uri {
static parse(input) {
let text = (input || '').trim().split('?')[0];
let data = { type: 'UNKNOWN' };
let error;
const toString = () => text;
const setError = (isError, message) => {
if (!error && isError) {
error = { type: 'URI', message, uri: text };
}
};
setError(!text, 'URI not specified');
const index = text.indexOf(':');
setError(index < 0, 'Not a valid multi-part URI');
if (!error) {
const left = text.substring(0, index);
const right = text.substring(index + 1).trim();
if (left === 'ns') {
const id = right;
setError(!id, 'Namespace URI identifier not found');
setError(!Uri.is.valid.ns(id), `URI contains an invalid "ns" identifier ("${id}")`);
const uri = { type: 'NS', id, toString };
data = uri;
}
else if (left === 'file') {
const id = right;
setError(!id, 'File URI identifier not found');
const parts = id.split(':');
const ns = (parts[0] || '').trim();
const file = (parts[1] || '').trim();
setError(!file, `File identifier within namespace "${ns}" not found`);
const uri = { type: 'FILE', id, ns, file, toString };
data = uri;
}
else if (left === 'cell') {
const id = right || '';
setError(!id, `ID of 'cell' not found`);
let type = 'CELL';
let key = '';
let ns = '';
if (!id.includes(':')) {
setError(true, `The 'cell' URI does not have a coordinate address, eg. ":A1" in "cell:foo:A1"`);
}
else {
const bang = id.replace(/\:/g, '!');
const parts = coord.cell.toCell(bang);
type = coord.cell.toType(bang);
key = parts.key;
ns = parts.ns;
setError(!key, `Coordinate key of '${type || '<empty>'}' not found`);
setError(!ns, `Coordinate namespace of '${type || '<empty>'}' not found`);
if (!error) {
text = toUri('cell', type, ns, key);
}
}
data = { type, id, ns, key, toString };
}
}
const ok = !Boolean(error) && data.type !== 'UNKNOWN';
const res = {
ok,
uri: text,
type: data.type,
parts: data,
toString,
};
return error ? Object.assign(Object.assign({}, res), { error }) : res;
}
}
Uri.cuid = cuid;
Uri.slug = slug;
Uri.ALLOW = Object.assign({}, DEFAULT.ALLOW);
Uri.create = {
ns: (id) => toUri('ns', 'NS', id),
cell: (ns, key) => toUri('cell', 'CELL', ns, key),
row: (ns, key) => toUri('cell', 'ROW', ns, key),
column: (ns, key) => toUri('cell', 'COLUMN', ns, key),
file: (ns, fileid) => toUri('file', 'FILE', ns, fileid),
};
Uri.is = {
uri: (input) => Uri.parse(input).ok,
type: (type, input) => {
const uri = Uri.parse(input);
return uri.parts.type === type && (type === 'UNKNOWN' ? true : uri.ok);
},
ns: (input) => Uri.is.type('NS', input),
file: (input) => Uri.is.type('FILE', input),
cell: (input) => Uri.is.type('CELL', input),
row: (input) => Uri.is.type('ROW', input),
column: (input) => Uri.is.type('COLUMN', input),
valid: {
ns(input) {
const value = (input || '').replace(/^ns\:/, '');
if (!value) {
return false;
}
if (isCuid(value)) {
return true;
}
const matchLegal = value.match(/^[A-Za-z0-9\.]*$/);
if (!matchLegal || (matchLegal && matchLegal[0] !== value)) {
return false;
}
return Uri.ALLOW.NS.some(pattern => {
return typeof pattern === 'string'
? pattern.includes('*')
? wildcard.isMatch(value, pattern)
: pattern === value
: pattern(value);
});
},
},
};
const alphaNumeric = new RegExp(/^[a-z0-9]+$/i);
function trimPrefix(prefix, input) {
const regex = new RegExp(`^${prefix}\:+`);
return input.trim().replace(regex, '');
}
function toUri(prefix, type, id, suffix) {
id = (id || '').trim();
id = id === ':' ? '' : id;
if (id) {
['ns', 'cell', 'file'].forEach(prefix => (id = trimPrefix(prefix, id)));
}
if (!id) {
throw new Error(`The "${prefix}" URI was not supplied with a namespace identifier. ("${id}")`);
}
if (!Uri.is.valid.ns(id)) {
const err = `URI contains an invalid "${prefix}" identifier, must be an alpha-numeric cuid. ("${id}")`;
throw new Error(err);
}
if (typeof suffix === 'string') {
suffix = (suffix || '').trim().replace(/^\:*/, '');
if (!suffix) {
throw new Error(`The "${prefix}" URI was not supplied with a suffix key.`);
}
if (prefix === 'file') {
if (!alphaNumeric.test(suffix)) {
const err = `The "file" URI contains an invalid file-identifier, must be alpha-numeric ("${suffix}").`;
throw new Error(err);
}
suffix = `:${suffix}`;
}
else {
if (suffix === 'undefined') {
const err = `The suffix "undefined" (string) was given - this it likely an upstream error where an [undefined] value has been converted into a string.`;
throw new Error(err);
}
suffix = suffix || '';
const suffixType = coord.cell.toType(suffix) || '';
if (suffixType !== type) {
const key = suffix || '';
const err = `The "${prefix}:" URI was not supplied with a valid ${type} key (given key "${key}").`;
throw new Error(err);
}
suffix = `:${suffix}`;
}
}
return `${prefix}:${id}${suffix || ''}`;
}
function isCuid(input) {
return input.length === 25 && input[0] === 'c' && alphaNumeric.test(input);
}