UNPKG

@lykmapipo/predefine

Version:

A representation of stored and retrieved information that does not qualify to belongs to their own domain model.

1,936 lines (1,775 loc) 68.4 kB
import { sortedUniq, permissionsFor, scopesFor, mergeObjects, randomColor, variableNameFor, areNotEmpty, idOf, flat, compact, uniq, pkg } from '@lykmapipo/common'; import { rcFor, getString, getStringSet, getObject, isTest, apiVersion as apiVersion$1 } from '@lykmapipo/env'; import { collectionNameOf, createSubSchema, copyInstance, createVarySubSchema, ObjectId, areSameObjectId, model, createSchema, Mixed, toObjectIds, areSameInstance, connect } from '@lykmapipo/mongoose-common'; import { mount } from '@lykmapipo/express-common'; import { Router, getFor, schemaFor, downloadFor, postFor, getByIdFor, patchFor, putFor, deleteFor, start as start$1 } from '@lykmapipo/express-rest-actions'; import { map, zipObject, without, keys, mapValues, pick, includes, omit, omitBy, isEmpty, toUpper, find, forEach, merge, get, flatMap, isMap, isFunction, isArray, trim, uniqWith } from 'lodash'; import { topology } from 'topojson-server'; import { localizedValuesFor, localizedIndexesFor, localize, localizedAbbreviationsFor, localizedKeysFor } from 'mongoose-locale-schema'; import { Point, LineString, Polygon, Geometry, MultiPoint, MultiLineString, MultiPolygon, GeometryCollection } from 'mongoose-geojson-schemas'; import { waterfall } from 'async'; import actions from 'mongoose-rest-actions'; import exportable from '@lykmapipo/mongoose-exportable'; // load rc for predefine const rc = rcFor('predefine'); const CONTENT_TYPE_JSON = 'json'; const CONTENT_TYPE_GEOJSON = 'geojson'; const CONTENT_TYPE_TOPOJSON = 'topojson'; const DEFAULT_LOCALE = getString('DEFAULT_LOCALE', 'en'); const LOCALES = getStringSet('LOCALES', DEFAULT_LOCALE); const MODEL_NAME = getString( 'PREDEFINE_MODEL_NAME', rc.modelName || 'Predefine' ); const COLLECTION_NAME = getString( 'PREDEFINE_COLLECTION_NAME', rc.collectionName || 'predefines' ); const SCHEMA_OPTIONS = { collection: COLLECTION_NAME }; const DEFAULT_NAMESPACE = getString( 'PREDEFINE_DEFAULT_NAMESPACE', rc.defaultNamespace || 'Setting' ); const DEFAULTS_BUCKET = getString( 'PREDEFINE_DEFAULTS_BUCKET', rc.defaultsBucket || 'defaults' ); const NAMESPACES = getStringSet( 'PREDEFINE_NAMESPACES', [DEFAULT_NAMESPACE].concat(rc.namespaces) ); const NAMESPACE_MAP = map(NAMESPACES, (namespace) => { return { namespace, bucket: collectionNameOf(namespace) }; }); const NAMESPACE_DICTIONARY = zipObject( NAMESPACES, map(NAMESPACES, (namespace) => collectionNameOf(namespace)) ); const DEFAULT_BUCKET = collectionNameOf(DEFAULT_NAMESPACE); const BUCKETS = sortedUniq(map(NAMESPACE_MAP, 'bucket')); const DOMAINS = getStringSet( 'PREDEFINE_DOMAINS', [...NAMESPACES].concat(rc.domains) ); const OPTION_SELECT = { 'strings.name': 1, 'strings.abbreviation': 1, 'strings.code': 1, 'strings.symbol': 1, 'strings.color': 1, 'numbers.weight': 1, 'booleans.default': 1, 'booleans.preset': 1, }; const OPTION_AUTOPOPULATE = getObject('PREDEFINE_AUTOPOPULATE_OPTION', { select: OPTION_SELECT, maxDepth: 1, }); const LOCALIZED_STRING_PATHS = ['name', 'abbreviation', 'description']; const DEFAULT_STRING_PATHS = [ { name: 'name', type: String, trim: true, index: true, searchable: true, taggable: true, exportable: true, localize: true, fake: (f) => f.commerce.productName(), }, { name: 'abbreviation', type: String, trim: true, index: true, searchable: true, taggable: true, exportable: true, localize: true, fake: (f) => toUpper(f.hacker.abbreviation()), }, { name: 'description', type: String, trim: true, index: true, searchable: true, exportable: true, localize: true, fake: (f) => f.lorem.sentence(), }, { name: 'code', type: String, trim: true, index: true, searchable: true, taggable: true, exportable: true, default: () => undefined, fake: (f) => f.finance.currencyCode(), }, { name: 'symbol', type: String, trim: true, index: true, searchable: true, taggable: true, exportable: true, default: () => undefined, fake: (f) => f.finance.currencySymbol(), }, { name: 'color', type: String, trim: true, uppercase: true, index: true, searchable: true, taggable: true, exportable: true, default: () => randomColor(), fake: () => randomColor(), }, { name: 'icon', type: String, trim: true, default: () => undefined, fake: (f) => f.image.image(), }, ]; const DEFAULT_NUMBER_PATHS = [ { name: 'weight', type: Number, index: true, default: () => 0, exportable: true, fake: (f) => f.random.number(), }, ]; const DEFAULT_BOOLEAN_PATHS = [ { name: 'default', type: Boolean, index: true, exportable: true, default: () => false, fake: (f) => f.random.boolean(), }, { name: 'preset', type: Boolean, index: true, exportable: true, default: () => false, fake: (f) => f.random.boolean(), }, { name: 'system', type: Boolean, index: true, exportable: true, default: () => false, fake: (f) => f.random.boolean(), }, ]; /** * @function uniqueIndexes * @name uniqueIndexes * @description Generate unique index definition of predefine * @returns {object} unique index definition * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.4.0 * @version 0.2.0 * @static * @public * @example * * uniqueIndexes(); * // => { 'name.en': 1, code: 1, bucket:1 } * */ const uniqueIndexes = () => { const indexes = mergeObjects( { namespace: 1, bucket: 1, domain: 1, 'relations.parent': 1, 'strings.code': 1, }, localizedIndexesFor('strings.name') ); return indexes; }; /** * @function ensureBucketAndNamespace * @name ensureBucketAndNamespace * @description Derive bucket and namespace of a given predefine bucker * or namespace. * @param {string} [bucketOrNamespace] valid predefine bucket or namespace * @returns {object} predefine bucket and namespace * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.9.0 * @version 0.1.0 * @static * @public * @example * * const val = ensureBucketAndNamespace(); * // => { bucket: ..., namespace: ... } * * const val = ensureBucketAndNamespace('Setting'); * // => { bucket: ..., namespace: ... }; * */ const ensureBucketAndNamespace = (bucketOrNamespace) => { // initialize defaults const defaults = isTest() ? {} : { bucket: DEFAULT_BUCKET, namespace: DEFAULT_NAMESPACE }; // derive bucket and namespace const bucketAndNamespace = mergeObjects( defaults, find(NAMESPACE_MAP, { namespace: bucketOrNamespace }), find(NAMESPACE_MAP, { bucket: bucketOrNamespace }) ); // return bucket and namespace return bucketAndNamespace; }; /** * @function parseNamespaceRelations * @name parseNamespaceRelations * @description Convert all specified namespace to relations * @returns {object} valid normalized relations * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.4.0 * @version 0.1.0 * @static * @public * @example * * parseNamespaceRelations(); * // => { setting: { type: ObjectId, ref: 'Predefine' } } * */ const parseNamespaceRelations = () => { // use namespace and parent let paths = map(NAMESPACES, (path) => variableNameFor(path)); paths = ['parent', ...paths]; // map relations to valid schema definitions let relations = zipObject(paths, paths); relations = mapValues(relations, () => { return mergeObjects({ type: ObjectId, ref: MODEL_NAME, index: true, exists: { refresh: true, select: OPTION_SELECT }, autopopulate: { maxDepth: 1, select: OPTION_SELECT }, taggable: true, // exportable: true, aggregatable: { unwind: true }, default: () => undefined, }); }); // return namespaces relations return relations; }; /** * @function parseGivenRelations * @name parseGivenRelations * @description Safely parse and normalize predefine relation config * @returns {object} valid normalized relations * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.4.0 * @version 0.1.0 * @static * @public * @example * * process.env.PREDEFINE_RELATIONS='{"owner":{"ref":"Party","array":true}}' * parseGivenRelations(); * // => { owner: { ref: 'Party', autopopulate:true } } * */ const parseGivenRelations = () => { let relations = getObject('PREDEFINE_RELATIONS', mergeObjects(rc.relations)); relations = mapValues(relations, (relation) => { const { ref = MODEL_NAME, array, autopopulate } = relation; // prepare population options const autopopulateOptns = ref === MODEL_NAME ? mergeObjects(autopopulate, OPTION_AUTOPOPULATE) : mergeObjects(autopopulate, { maxDepth: 1 }); // prepare relation schema const relationSchema = mergeObjects(relation, { type: array ? [{ type: ObjectId, ref }] : ObjectId, ref, index: true, autopopulate: autopopulateOptns, taggable: true, // TODO: exportable: true, aggregatable: { unwind: true }, duplicate: areSameObjectId, default: () => undefined, }); // return relation schema return relationSchema; }); // return parsed relations return relations; }; /** * @function relationSchemaPaths * @name relationSchemaPaths * @description Expose schema relation paths * @returns {Array} set of relation paths * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 1.6.0 * @version 0.1.0 * @static * @public * @example * * const paths = relationSchemaPaths(); * // => ['parent', ... ]; * */ const relationSchemaPaths = () => { // obtain ignored relations const ignoredNamespaces = getStringSet('PREDEFINE_RELATIONS_IGNORED', []); const ignoredPaths = map(ignoredNamespaces, (path) => variableNameFor(path)); const ignoredRelations = [...ignoredNamespaces, ...ignoredPaths]; // parse relations const relations = mergeObjects( parseGivenRelations(), parseNamespaceRelations() ); // remove ignored const allowedRelations = omitBy(relations, ({ ref }, key) => { return includes(ignoredRelations, key) || includes(ignoredRelations, ref); }); // allow relations paths return sortedUniq([...keys(allowedRelations)]); }; /** * @function createRelationsSchema * @name createRelationsSchema * @description Create predefine relations schema * @returns {object} valid mongoose schema * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.4.0 * @version 0.1.0 * @static * @public * @example * * createRelationsSchema(); * */ const createRelationsSchema = () => { // obtain ignored relations const ignoredNamespaces = getStringSet('PREDEFINE_RELATIONS_IGNORED', []); const ignoredPaths = map(ignoredNamespaces, (path) => variableNameFor(path)); const ignoredRelations = [...ignoredNamespaces, ...ignoredPaths]; // derive relations const relations = mergeObjects( parseGivenRelations(), parseNamespaceRelations() ); // remove ignored const allowedRelations = omitBy(relations, ({ ref }, key) => { return includes(ignoredRelations, key) || includes(ignoredRelations, ref); }); const relationsSubSchema = createSubSchema(allowedRelations); return relationsSubSchema; }; /** * @function stringsDefaultValue * @name stringsDefaultValue * @description Expose string paths, default values. * @param {object} [values] valid string paths, values. * @returns {object} hash of string paths, default values. * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.9.0 * @version 0.1.0 * @static * @public * @example * * const values = stringsDefaultValue(); * // => { code: 'UA', color: '#CCCCCC', ... }; * */ const stringsDefaultValue = (values) => { // initialize defaults let defaults = {}; // compute string defaults forEach(DEFAULT_STRING_PATHS, (path) => { defaults[path.name] = path.default && path.default(); }); // merge given defaults = mergeObjects(defaults, copyInstance(values)); // return string paths, default values return defaults; }; /** * @function stringSchemaPaths * @name stringSchemaPaths * @description Expose schema string paths * @returns {Array} set of string paths * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.9.0 * @version 0.1.0 * @static * @public * @example * * const paths = stringSchemaPaths(); * // => ['code', 'symbol', 'color', 'icon', ... ]; * */ const stringSchemaPaths = () => sortedUniq([ ...map(DEFAULT_STRING_PATHS, 'name'), ...getStringSet('PREDEFINE_STRINGS', [].concat(rc.strings)), ]); /** * @function createStringsSchema * @name createStringsSchema * @description Create predefine strings schema * @returns {object} valid mongoose schema * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.9.0 * @version 0.1.0 * @static * @public * @example * * const strings = createStringsSchema(); * // => { code: { type: String, ... }, ... } * */ const createStringsSchema = () => { // prepare strings schema path options const options = { type: String, trim: true, index: true, searchable: true, taggable: true, exportable: true, fake: (f) => f.commerce.productName(), }; // obtain given strings schema paths let givenPaths = without( stringSchemaPaths(), ...map(DEFAULT_STRING_PATHS, 'name') ); // convert given paths to schema definition givenPaths = map(givenPaths, (givenPath) => { return mergeObjects(options, { name: givenPath }); }); // merge defaults with given string paths const paths = [...DEFAULT_STRING_PATHS, ...givenPaths]; // build stings schema definition const definition = {}; forEach(paths, (path) => { const { name, ...optns } = path; definition[path.name] = optns.localize ? localize(optns) : optns; }); // create strings sub schema const schema = createSubSchema(definition); // return strings sub schema return schema; }; /** * @function numbersDefaultValue * @name numbersDefaultValue * @description Expose number paths, default values. * @param {object} [values] valid number paths, values. * @returns {object} hash of number paths, default values. * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.9.0 * @version 0.1.0 * @static * @public * @example * * const values = numbersDefaultValue(); * // => { default: false, preset: false, ... }; * */ const numbersDefaultValue = (values) => { // initialize defaults let defaults = {}; // compute number defaults forEach(DEFAULT_NUMBER_PATHS, (path) => { defaults[path.name] = path.default(); }); // merge given defaults = merge(defaults, copyInstance(values)); // return number paths, default values return defaults; }; /** * @function numberSchemaPaths * @name numberSchemaPaths * @description Expose schema number paths * @returns {Array} set of number paths * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.9.0 * @version 0.1.0 * @static * @public * @example * * const paths = numberSchemaPaths(); * // => ['weight', ... ]; * */ const numberSchemaPaths = () => sortedUniq([ ...map(DEFAULT_NUMBER_PATHS, 'name'), ...getStringSet('PREDEFINE_NUMBERS', [].concat(rc.numbers)), ]); /** * @function createNumbersSchema * @name createNumbersSchema * @description Create predefine numbers schema * @returns {object} valid mongoose schema * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.9.0 * @version 0.1.0 * @static * @public * @example * * const numbers = createNumbersSchema(); * // => { weight: { type: Number, ... }, ... } * */ const createNumbersSchema = () => { // obtain given numbers schema paths const givenPaths = without( numberSchemaPaths(), ...map(DEFAULT_NUMBER_PATHS, 'name') ); // merge defaults with given number paths const paths = [...DEFAULT_NUMBER_PATHS, ...givenPaths]; // prepare numbers schema path options const options = { type: Number, index: true, exportable: true, fake: (f) => f.random.number(), }; // create numbers sub schema const schema = createVarySubSchema(options, ...paths); // return numbers sub schema return schema; }; /** * @function booleanSchemaPaths * @name booleanSchemaPaths * @description Expose schema boolean paths * @returns {Array} set of boolean paths * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.9.0 * @version 0.1.0 * @static * @public * @example * * const paths = booleanSchemaPaths(); * // => ['default', 'preset', ... ]; * */ const booleanSchemaPaths = () => sortedUniq([ ...map(DEFAULT_BOOLEAN_PATHS, 'name'), ...getStringSet('PREDEFINE_BOOLEANS', [].concat(rc.booleans)), ]); /** * @function booleansDefaultValue * @name booleansDefaultValue * @description Expose boolean paths, default values. * @param {object} [values] valid boolean paths, values. * @returns {object} hash of boolean paths, default values. * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.9.0 * @version 0.1.0 * @static * @public * @example * * const values = booleansDefaultValue(); * // => { default: false, preset: false, ... }; * */ const booleansDefaultValue = (values) => { // initialize defaults let defaults = {}; // compute boolean defaults forEach(DEFAULT_BOOLEAN_PATHS, (path) => { defaults[path.name] = path.default(); }); // merge given defaults = mergeObjects(defaults, copyInstance(values)); // return boolean paths, default values return defaults; }; /** * @function createBooleansSchema * @name createBooleansSchema * @description Create predefine booleans schema * @returns {object} valid mongoose schema * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.9.0 * @version 0.1.0 * @static * @public * @example * * const booleans = createBooleansSchema(); * // => { default: { type: Boolean, ... }, ... } * */ const createBooleansSchema = () => { // obtain given booleans schema paths const givenPaths = without( booleanSchemaPaths(), ...map(DEFAULT_BOOLEAN_PATHS, 'name') ); // merge defaults with given boolean paths const paths = [...DEFAULT_BOOLEAN_PATHS, ...givenPaths]; // prepare booleans schema path options const options = { type: Boolean, index: true, exportable: true, fake: (f) => f.random.boolean(), }; // create booleans sub schema const schema = createVarySubSchema(options, ...paths); // return booleans sub schema return schema; }; /** * @function dateSchemaPaths * @name dateSchemaPaths * @description Expose schema date paths * @returns {Array} set of date paths * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 1.5.0 * @version 0.1.0 * @static * @public * @example * * const paths = dateSchemaPaths(); * // => ['issuedAt']; * */ const dateSchemaPaths = () => sortedUniq([...getStringSet('PREDEFINE_DATES', [].concat(rc.dates))]); /** * @function createDatesSchema * @name createDatesSchema * @description Create predefine dates schema * @returns {object} valid mongoose schema * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.9.0 * @version 0.1.0 * @static * @public * @example * * const dates = createDatesSchema(); * // => { startedAt: { type: Date, ... }, ... } * */ const createDatesSchema = () => { // obtain dates schema paths const dates = sortedUniq([...dateSchemaPaths()]); // prepare dates schema path options const options = { type: Date, index: true, exportable: true, fake: (f) => f.date.recent(), }; // create dates sub schema const schema = createVarySubSchema(options, ...dates); // return dates sub schema return schema; }; /** * @function geoSchemaPaths * @name geoSchemaPaths * @description Expose schema geo paths * @returns {Array} set of geo paths * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.9.0 * @version 0.1.0 * @static * @public * @example * * const paths = geoSchemaPaths(); * // => ['point', ... ]; * */ const geoSchemaPaths = () => sortedUniq([ 'point', 'line', 'polygon', 'geometry', 'points', 'lines', 'polygons', 'geometries', ]); /** * @function createGeosSchema * @name createGeosSchema * @description Create predefine geos schema * @returns {object} valid mongoose schema * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.9.0 * @version 0.1.0 * @static * @public * @example * * const dates = createGeosSchema(); * // => { point: { type: Date, ... }, ... } * */ const createGeosSchema = () => { // prepare geos schema path options const geos = { point: Point, line: LineString, polygon: Polygon, geometry: Geometry, points: MultiPoint, lines: MultiLineString, polygons: MultiPolygon, geometries: GeometryCollection, }; // create geos sub schema const schema = createSubSchema(geos); // return geos sub schema return schema; }; /** * @function listPermissions * @name listPermissions * @description Generate predefine permissions * @param {...string} [ignored] valid ignored namespaces * @returns {object[]} valid permissions * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.9.0 * @version 0.1.0 * @static * @public * @example * * const permissions = listPermissions(); * // => [{resource: 'Setting', wildcard: 'setting:create', action: ...}, ....]; * */ const listPermissions = (...ignored) => { // collect allowed namespace resource const resources = sortedUniq(without(NAMESPACES, ...ignored)); // generate resources permissions const permissions = permissionsFor(...resources); // return predefine permissions return permissions; }; /** * @function listScopes * @name listScopes * @description Generate predefine scopes * @param {...string} [ignored] valid ignored namespaces * @returns {string[]} valid scopes * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.9.0 * @version 0.1.0 * @static * @public * @example * * const scopes = listScopes(); * // => ['setting:create', ....]; * */ const listScopes = (...ignored) => { // collect allowed namespace resource const resources = sortedUniq(without(NAMESPACES, ...ignored)); // generate resources scopes const scopes = scopesFor(...resources); // return predefine scopes return scopes; }; /** * @function normalizeQueryFilter * @name normalizeQueryFilter * @description Normalize query filter * @param {object} [optns={}] valid query filter * @returns {object} normalized query filter * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.9.0 * @version 0.1.0 * @static * @private * @example * * const filter = normalizeQueryFilter(mquery); * // => { ... }; * */ const normalizeQueryFilter = (optns = {}) => { let options = mergeObjects(optns); const isDefaultBucket = get(options, 'filter.bucket') === DEFAULTS_BUCKET; if (isDefaultBucket) { options = omit(options, 'filter.bucket'); const paginate = { limit: Number.MAX_SAFE_INTEGER }; const filter = { 'booleans.default': true }; options = mergeObjects(options, { filter, paginate }); } return options; }; /** * @function mapToGeoJSONFeature * @name mapToGeoJSONFeature * @description Transform predefine to GeoJSON feature(s) * @param {object} predefine valid predefine instance * @returns {object} GeoJSON feature(s) * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.9.0 * @version 0.1.0 * @static * @private * @example * * const feature = mapToGeoJSONFeature(predefine); * // => { type: 'Feature', geometry: ..., properties: ... }; * */ const mapToGeoJSONFeature = (predefine = {}) => { // copy predefine const { _id, namespace, bucket, strings, numbers, booleans, dates, geos, relations, properties: props, } = copyInstance(predefine); // prepare properties const properties = { namespace, bucket, strings, numbers, booleans, dates, relations, properties: isMap(props) ? Object.fromEntries(props) : props, }; // derive feature(s) const type = 'Feature'; const features = map(geos, (geometry, path) => { const id = `${path}:${_id}`; return { _id, id, type, properties, geometry }; }); // return geojson features return features; }; /** * @function mapToGeoJSONFeatureCollection * @name mapToGeoJSONFeatureCollection * @description Transform predefines to GeoJSON feature collection. * @param {...object} predefines valid predefines instance * @returns {object} GeoJSON feature collection * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.9.0 * @version 0.1.0 * @static * @private * @example * * const features = mapToGeoJSONFeatureCollection(predefine); * // => { type: 'FeatureCollection', features: [ ... ], ... }; * */ const mapToGeoJSONFeatureCollection = (...predefines) => { // map predefines to features const features = flatMap([...predefines], mapToGeoJSONFeature); // derive geojson feature collection const type = 'FeatureCollection'; const collections = { type, features }; // return geojson feature collections return collections; }; /** * @function mapToTopoJSON * @name mapToTopoJSON * @description Transform predefines to topojson. * @param {...object} predefines valid predefines instance * @returns {object} valid topojson * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.9.0 * @version 0.1.0 * @static * @private * @example * * const topojson = mapToTopoJSON(predefine); * // => { type: 'Topology', objects: [ ... ], ... }; * */ const mapToTopoJSON = (...predefines) => { // map predefines to feature collections const collection = mapToGeoJSONFeatureCollection(...predefines); // derive topojson const topojson = topology({ collection }); // return topojson return topojson; }; /** * @function transformToPredefine * @name transformToPredefine * @description Transform given plain object to predefine structure. * @param {object} [val] valid plain object to transform * @returns {object} valid predefine plain object * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 1.5.0 * @version 0.3.0 * @static * @public * @example * * transformToPredefine({ name: 'John Doe' }); * // => { name : { en: 'John Doe' }, ... }; * */ const transformToPredefine = (val) => { // ensure data const data = mergeObjects(val); // obtain paths const stringPaths = stringSchemaPaths(); const numberPaths = numberSchemaPaths(); const booleanPaths = booleanSchemaPaths(); const datePaths = dateSchemaPaths(); const geoPaths = geoSchemaPaths(); const relationPaths = relationSchemaPaths(); const knownPaths = [ ...stringPaths, ...numberPaths, ...booleanPaths, ...datePaths, ...geoPaths, ...relationPaths, 'namespace', 'bucket', 'domain', 'populate', 'strings', 'numbers', 'booleans', 'dates', 'geos', 'properties', 'relations', ]; // transform to predefine let predefine = mergeObjects({ namespace: data.namespace, bucket: data.bucket, domain: data.domain, strings: mapValues(pick(data, ...stringPaths), (value, key) => { if (includes(LOCALIZED_STRING_PATHS, key)) { return localizedValuesFor({ en: value }); } return value; }), numbers: pick(data, ...numberPaths), booleans: pick(data, ...booleanPaths), dates: pick(data, ...datePaths), geos: pick(data, ...geoPaths), relations: pick(data, relationPaths), properties: omit( mergeObjects(data.properties, omit(data, ...knownPaths)), ...knownPaths ), }); // omit empty paths predefine = omitBy(predefine, isEmpty); // return return predefine; }; /** * @function checkIfBucketExists * @name checkIfBucketExists * @description Check if bucket exists or allowed. * @param {string} bucket valid bucket name * @param {Function} done callback to invoke on success or error * @returns {Error} error if not exists else true * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 1.7.0 * @version 0.1.0 * @static * @public * @example * * checkIfBucketExists('settings', error => { ... }); * */ const checkIfBucketExists = (bucket, done) => { const bucketSet = [DEFAULTS_BUCKET, ...BUCKETS]; const bucketExist = bucket && includes(bucketSet, bucket); if (bucket && !bucketExist) { const error = new Error('Not Found'); error.status = 404; error.code = 404; error.description = `${bucket} bucket does not exists`; return done(error); } return done(null, true); }; // TODO: load from PREDEFINE_PLUGINS // TODO: load from .rc plugins // TODO: create, update by namespace /** * @function fakeByNamespace * @name fakeByNamespace * @description Schema plugin to extend predefine faking by namespace * @param {object} schema valid mongoose schema * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 1.9.0 * @version 0.2.0 * @static * @public * @example * * // plug into schema * PredefineSchema.plugin(fakeByNamespace); * * // use alias * Predefine.fakeSetting(); //=> Predefine{...} */ const fakeByNamespace = (schema) => { // use namespace map to build namespaced faker forEach(NAMESPACE_MAP, (predefine) => { const { namespace, bucket } = predefine; // ensure namespace and bucket if (areNotEmpty(namespace, bucket)) { // derive namespace faker method name const methodName = `fake${namespace}`; // check if namespaced faker exists if (!isFunction(schema.statics[methodName])) { // extend schema with namespaced fakers schema.static({ // args: size = 1, locale = 'en', only = undefined, except = undefined [methodName](...args) { // no-arrow // this: refer to model static context const fakes = this.fake(...args); // update with namespace and bucket if (isArray(fakes)) { forEach(fakes, (fake) => { fake.set({ namespace, bucket }); return fake; }); } else { fakes.set({ namespace, bucket }); } // return fakes return fakes; }, }); } } }); }; /** * @function findByNamespace * @name findByNamespace * @description Schema plugin to extend predefine find by namespace * @param {object} schema valid mongoose schema * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 1.12.0 * @version 0.1.0 * @static * @public * @example * * // plug into schema * PredefineSchema.plugin(findByNamespace); * * // use alias * Predefine.findSetting(); //=> Query{...} */ const findByNamespace = (schema) => { // use namespace map to build namespaced finder forEach(NAMESPACE_MAP, (predefine) => { const { namespace, bucket } = predefine; // ensure namespace and bucket if (areNotEmpty(namespace, bucket)) { // derive namespace finder method name const methodName = `find${namespace}`; // check if namespaced finder exists if (!isFunction(schema.statics[methodName])) { // extend schema with namespaced finder schema.static({ // args: filter, [projection], [options], [callback] [methodName](filter, projection, options, callback) { // no-arrow // this: refer to model static context // ensure namespace and bucket into filter const actualFilter = isFunction(filter) ? mergeObjects({ namespace, bucket }) : mergeObjects({ namespace, bucket }, filter); // check for callback const actualCallback = find( [filter, projection, options, callback], isFunction ); // return return this.find(actualFilter, projection, options, actualCallback); }, }); } } }); }; /** * @function findRecursiveByNamespace * @name findRecursiveByNamespace * @description Schema plugin to extend predefine find recursive by namespace * @param {object} schema valid mongoose schema * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 1.12.0 * @version 0.1.0 * @static * @public * @example * * // plug into schema * PredefineSchema.plugin(findRecursiveByNamespace); * * // use alias * Predefine.findSettingParents(); //=> Query{...} * * Predefine.findSettingChildren(); //=> Query{...} * */ const findRecursiveByNamespace = (schema) => { // use namespace map to build namespaced recursive finder forEach(NAMESPACE_MAP, (predefine) => { const { namespace, bucket } = predefine; // ensure namespace and bucket if (areNotEmpty(namespace, bucket)) { // derive recursive namespace finder methods name const parentsMethodName = `find${namespace}Parents`; const childrenMethodName = `find${namespace}Children`; // check if namespaced parents recursive finder exists if (!isFunction(schema.statics[parentsMethodName])) { // extend schema with namespaced recursive parents finder schema.static({ // args: criteria, [callback] [parentsMethodName](criteria, callback) { // no-arrow // this: refer to model static context // ensure namespace and bucket into filter // ensure namespace and bucket into criteria const actualCriteria = mergeObjects( { namespace, bucket }, criteria ); // return return this.findParents(actualCriteria, callback); }, }); } // check if namespaced children recursive finder exists if (!isFunction(schema.statics[childrenMethodName])) { // extend schema with namespaced recursive children finder schema.static({ // args: criteria, [callback] [childrenMethodName](criteria, callback) { // no-arrow // this: refer to model static context // ensure namespace and bucket into criteria const actualCriteria = mergeObjects( { namespace, bucket }, criteria ); // return return this.findChildren(actualCriteria, callback); }, }); } } }); }; /** * @function findOneByNamespace * @name findOneByNamespace * @description Schema plugin to extend predefine findOne by namespace * @param {object} schema valid mongoose schema * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 1.15.0 * @version 0.1.0 * @static * @public * @example * * // plug into schema * PredefineSchema.plugin(findOneByNamespace); * * // use alias * Predefine.findOneSetting(); //=> Query{...} */ const findOneByNamespace = (schema) => { // use namespace map to build namespaced finder forEach(NAMESPACE_MAP, (predefine) => { const { namespace, bucket } = predefine; // ensure namespace and bucket if (areNotEmpty(namespace, bucket)) { // derive namespace finder one method name // TODO: findById const methodName = `findOne${namespace}`; // check if namespaced finder exists if (!isFunction(schema.statics[methodName])) { // extend schema with namespaced finder schema.static({ // args: [conditions], [projection], [options], [callback] [methodName](conditions, projection, options, callback) { // no-arrow // this: refer to model static context // ensure namespace and bucket into conditions const actualConditions = isFunction(conditions) ? mergeObjects({ namespace, bucket }) : mergeObjects({ namespace, bucket }, conditions); // check for callback const actualCallback = find( [conditions, projection, options, callback], isFunction ); // return return this.findOne( actualConditions, projection, options, actualCallback ); }, }); } } }); }; /** * @module Predefine * @name Predefine * @description A representation of stored and retrieved information * that does not qualify to belongs to their own domain model. * * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.1.0 * @version 0.1.0 * @public * @example * * const { Predefine } = require('@lykmapipo/predefine'); * * const unit = { * namespace: 'Unit', * strings: { * name: { en: 'Kilogram' }, * code: 'Kg', * abbreviation: { en: 'Kg' } * } * }; * Predefine.create(unit, (error, created) => { ... }); * */ const PredefineSchema = createSchema( { /** * @name namespace * @description Machine readable namespace of a predefined. * * Its a super-class when do data modelling. * * @type {object} * @property {object} type - schema(data) type * @property {boolean} trim - force trimming * @property {boolean} required - mark field as required * @property {boolean} enum - list of acceptable values * @property {boolean} index - ensure database index * @property {boolean} searchable - allow for searching * @property {boolean} taggable - allow field use for tagging * @property {boolean} hide - mark field as hidden * @property {boolean} default - default value set when none provided * @property {object|boolean} fake - fake data generator options * * @since 0.1.0 * @version 0.1.0 * @instance * @example * Unit, Currency * */ namespace: { type: String, trim: true, required: true, enum: NAMESPACES, index: true, searchable: true, taggable: true, hide: !isTest(), fake: true, }, /** * @name bucket * @alias collection * @alias key * @description Machine readable collection name of a predefine. * * Its a table or collection name as when do database modelling. * * @type {object} * @property {object} type - schema(data) type * @property {boolean} required - mark required * @property {boolean} trim - force trimming * @property {boolean} enum - list of acceptable values * @property {boolean} index - ensure database index * @property {boolean} searchable - allow for searching * @property {boolean} taggable - allow field use for tagging * @property {boolean} hide - mark field as hidden * @property {boolean} default - default value set when none provided * @property {object|boolean} fake - fake data generator options * * @author lally elias <lallyelias87@gmail.com> * @since 0.1.0 * @version 0.1.0 * @instance * @example * units, currencies * */ bucket: { type: String, trim: true, required: true, enum: BUCKETS, index: true, searchable: true, taggable: true, hide: !isTest(), fake: true, }, /** * @name domain * @description Human readable domain of a predefined. * * Its a sub-class inherit from super-class(namespace) when do * data modelling. * * @type {object} * @property {object} type - schema(data) type * @property {boolean} trim - force trimming * @property {boolean} enum - list of acceptable values * @property {boolean} index - ensure database index * @property {boolean} searchable - allow for searching * @property {boolean} taggable - allow field use for tagging * @property {boolean} hide - mark field as hidden * @property {boolean} default - default value set when none provided * @property {object|boolean} fake - fake data generator options * * @since 1.19.0 * @version 0.1.0 * @instance * @example * Unit, Currency * */ domain: { type: String, trim: true, enum: DOMAINS, index: true, searchable: true, taggable: true, hide: !isTest(), fake: true, }, /** * @name strings * @description A map of strings to allow storing vary string fields to * a predefined. * * @type {object} * * @since 0.9.0 * @version 0.1.0 * @instance * @example * { * "code": "12TRTE" * "symbol": "$", * "color": "#CCCCCC" * } * */ strings: createStringsSchema(), /** * @name numbers * @description A map of numbers to allow storing vary number fields to * a predefined. * * @type {object} * * @since 0.9.0 * @version 0.1.0 * @instance * @example * { * "weight": 1 * "steps": 1 * } * */ numbers: createNumbersSchema(), /** * @name booleans * @description A map of booleans to allow storing vary boolean fields to * a predefined. * * @type {object} * * @since 0.9.0 * @version 0.1.0 * @instance * @example * { * "default": true * "preset": true * } * */ booleans: createBooleansSchema(), /** * @name dates * @description A map of dates to allow storing vary date fields to * a predefined. * * @type {object} * * @since 0.9.0 * @version 0.1.0 * @instance * @example * { * "startedAt": "2018-09-17T00:23:38.000Z" * "endedAt": "2019-09-17T09:23:38.046Z" * } * */ dates: createDatesSchema(), /** * @name geos * @description A map of geometries to allow storing vary geo fields to * a predefined. * * @type {object} * * @since 0.9.0 * @version 0.1.0 * @instance * @example * { * point: { * type: 'Point', * coordinates: [-76.80207859497996, 55.69469494228919] * } * } * */ geos: createGeosSchema(), /** * @name relations * @description Map of logical associated values of a predefined. They * reprents 1-to-1 relationship of other domain models with a predefine. * * @type {object} * * @since 0.4.0 * @version 0.1.0 * @instance * @example * { * "category": "5ce1a93ba7e7a56060e43997" * "unit": "5ce1a93ba7e7a56060e42981" * } * */ relations: createRelationsSchema(), /** * @name properties * @description A map of key value pairs to allow to associate * other meaningful information to a predefined. * * @type {object} * @property {object} type - schema(data) type * @property {object} fake - fake data generator options * * @since 0.1.0 * @version 0.1.0 * @instance * @example * { * "population": { * "male": 1700000, * "female": 2700000 * } * } * */ properties: { type: Map, of: Mixed, fake: (f) => f.helpers.createTransaction(), }, }, SCHEMA_OPTIONS, actions, exportable, findByNamespace, findRecursiveByNamespace, findOneByNamespace, fakeByNamespace ); /* *------------------------------------------------------------------------------ * Indexes *------------------------------------------------------------------------------ */ /** * @name index * @description ensure unique compound index on resource and action * to force unique predefine definition * * @author lally elias <lallyelias87@gmail.com> * @since 0.1.0 * @version 0.1.0 * @private */ PredefineSchema.index(uniqueIndexes(), { unique: true }); /* *------------------------------------------------------------------------------ * Hooks *------------------------------------------------------------------------------ */ /** * @name validate * @function validate * @description predefine schema pre validation hook * @param {Function} done callback to invoke on success or error * @returns {object|Error} valid instance or error * * @author lally elias <lallyelias87@gmail.com> * @since 0.1.0 * @version 0.1.0 * @private */ PredefineSchema.pre('validate', function onPreValidate(done) { return this.preValidate(done); }); /* *------------------------------------------------------------------------------ * Instance *------------------------------------------------------------------------------ */ /** * @name preValidate * @function preValidate * @description predefine schema pre validation hook logic * @param {Function} done callback to invoke on success or error * @returns {object|Error} valid instance or error * * @author lally elias <lallyelias87@gmail.com> * @since 0.1.0 * @version 0.1.0 * @instance */ PredefineSchema.methods.preValidate = function preValidate(done) { // ensure strings, values this.strings = stringsDefaultValue(this.strings); // ensure numbers, values this.numbers = numbersDefaultValue(this.numbers); // ensure booleans, values this.booleans = booleansDefaultValue(this.booleans); // ensure name for all locales this.strings.name = localizedValuesFor(this.strings.name); // ensure description for all locales this.strings.description = mergeObjects( localizedValuesFor(this.strings.name), localizedValuesFor(this.strings.description) ); // ensure abbreviation for all locales this.strings.abbreviation = mergeObjects( localizedAbbreviationsFor(this.strings.name), localizedValuesFor(this.strings.abbreviation) ); // ensure correct namespace and bucket const bucketOrNamespace = this.bucket || this.namespace; const bucketAndNamespace = ensureBucketAndNamespace(bucketOrNamespace); this.set(bucketAndNamespace); // ensure domain this.domain = this.domain || this.namespace; // ensure code this.strings.code = trim(this.strings.code) || this.strings.abbreviation[DEFAULT_LOCALE]; // continue return done(null, this); }; /* *------------------------------------------------------------------------------ * Statics *------------------------------------------------------------------------------ */ /* static constants */ PredefineSchema.statics.MODEL_NAME = MODEL_NAME; PredefineSchema.statics.COLLECTION_NAME = COLLECTION_NAME; PredefineSchema.statics.DEFAULT_NAMESPACE = DEFAULT_NAMESPACE; PredefineSchema.statics.OPTION_SELECT = OPTION_SELECT; PredefineSchema.statics.OPTION_AUTOPOPULATE = OPTION_AUTOPOPULATE; PredefineSchema.statics.NAMESPACES = NAMESPACES; PredefineSchema.statics.DEFAULT_BUCKET = DEFAULT_BUCKET; PredefineSchema.statics.BUCKETS = BUCKETS; /** * @name prepareSeedCriteria * @function prepareSeedCriteria * @description define seed data criteria * @param {object} seed predefined to be seeded * @returns {object} packed criteria for seeding * * @author lally elias <lallyelias87@gmail.com> * @since 0.2.0 * @version 0.1.0 * @static */ PredefineSchema.statics.prepareSeedCriteria = (seed) => { // TODO: convert seed to object if is instance // try use seed id as criteria if exists const id = idOf(seed); if (!isEmpty(id)) { return { _id: id }; } // otherwise use fields and releations for criteria let criteria = {}; const copyOfSeed = seed; copyOfSeed.name = localizedValuesFor(get(seed, 'strings.name')); // use fields to criteria const names = localizedKeysFor('strings.name'); const fieldsCriteria = flat( pick(copyOfSeed, 'namespace', 'bucket', 'domain', 'strings.code', ...names) ); criteria = mergeObjects(criteria, fieldsCriteria); // use non-empty relations to criteria const relationPaths = relationSchemaPaths(); const relationsCriteria = {}; forEach(relationPaths, (relationPath) => { // derive actual relation schema path const actualRelationPath = `relations.${relationPath}`; // collect relation value & convert to _id let relationValue = get(seed, actualRelationPath); relationValue = isArray(relationValue) ? map(relationValue, (val) => idOf(val)) : idOf(relationValue); // set relation relationsCriteria[actualRelationPath] = relationValue; }); criteria = mergeObjects(criteria, relationsCriteria); // return merged criteria return criteria; }; /** * @name getOneOrDefault * @function getOneOrDefault * @description Find existing predefine or default based on given criteria * @param {object} criteria valid query criteria * @param {Function} done callback to invoke on success or error * @returns {object|Error} found model or error * * @author lally elias <lallyelias87@gmail.com> * @since 0.6.0 * @version 0.1.0 * @static * @example * * const criteria = { bucket: 'settings', _id: '...'}; * Predefine.getOneOrDefault(criteria, (error, found) => { ... }); * */ PredefineSchema.statics.getOneOrDefault = (criteria, done) => { // normalize criteria const { _id, namespace, bucket, domain, ...filters } = mergeObjects(criteria); const allowDefault = !isEmpty(namespace || bucket); const allowId = !isEmpty(_id); const allowFilters = !isEmpty(filters); const byDefault = mergeObjects({ namespace, bucket, domain, 'booleans.default': true, }); const byId