123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189 |
- 'use strict';
- const { cppdb } = require('../util');
- module.exports = function defineTable(name, factory) {
- // Validate arguments
- if (typeof name !== 'string') throw new TypeError('Expected first argument to be a string');
- if (!name) throw new TypeError('Virtual table module name cannot be an empty string');
- // Determine whether the module is eponymous-only or not
- let eponymous = false;
- if (typeof factory === 'object' && factory !== null) {
- eponymous = true;
- factory = defer(parseTableDefinition(factory, 'used', name));
- } else {
- if (typeof factory !== 'function') throw new TypeError('Expected second argument to be a function or a table definition object');
- factory = wrapFactory(factory);
- }
- this[cppdb].table(factory, name, eponymous);
- return this;
- };
- function wrapFactory(factory) {
- return function virtualTableFactory(moduleName, databaseName, tableName, ...args) {
- const thisObject = {
- module: moduleName,
- database: databaseName,
- table: tableName,
- };
- // Generate a new table definition by invoking the factory
- const def = apply.call(factory, thisObject, args);
- if (typeof def !== 'object' || def === null) {
- throw new TypeError(`Virtual table module "${moduleName}" did not return a table definition object`);
- }
- return parseTableDefinition(def, 'returned', moduleName);
- };
- }
- function parseTableDefinition(def, verb, moduleName) {
- // Validate required properties
- if (!hasOwnProperty.call(def, 'rows')) {
- throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition without a "rows" property`);
- }
- if (!hasOwnProperty.call(def, 'columns')) {
- throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition without a "columns" property`);
- }
- // Validate "rows" property
- const rows = def.rows;
- if (typeof rows !== 'function' || Object.getPrototypeOf(rows) !== GeneratorFunctionPrototype) {
- throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with an invalid "rows" property (should be a generator function)`);
- }
- // Validate "columns" property
- let columns = def.columns;
- if (!Array.isArray(columns) || !(columns = [...columns]).every(x => typeof x === 'string')) {
- throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with an invalid "columns" property (should be an array of strings)`);
- }
- if (columns.length !== new Set(columns).size) {
- throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with duplicate column names`);
- }
- if (!columns.length) {
- throw new RangeError(`Virtual table module "${moduleName}" ${verb} a table definition with zero columns`);
- }
- // Validate "parameters" property
- let parameters;
- if (hasOwnProperty.call(def, 'parameters')) {
- parameters = def.parameters;
- if (!Array.isArray(parameters) || !(parameters = [...parameters]).every(x => typeof x === 'string')) {
- throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with an invalid "parameters" property (should be an array of strings)`);
- }
- } else {
- parameters = inferParameters(rows);
- }
- if (parameters.length !== new Set(parameters).size) {
- throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with duplicate parameter names`);
- }
- if (parameters.length > 32) {
- throw new RangeError(`Virtual table module "${moduleName}" ${verb} a table definition with more than the maximum number of 32 parameters`);
- }
- for (const parameter of parameters) {
- if (columns.includes(parameter)) {
- throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with column "${parameter}" which was ambiguously defined as both a column and parameter`);
- }
- }
- // Validate "safeIntegers" option
- let safeIntegers = 2;
- if (hasOwnProperty.call(def, 'safeIntegers')) {
- const bool = def.safeIntegers;
- if (typeof bool !== 'boolean') {
- throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with an invalid "safeIntegers" property (should be a boolean)`);
- }
- safeIntegers = +bool;
- }
- // Validate "directOnly" option
- let directOnly = false;
- if (hasOwnProperty.call(def, 'directOnly')) {
- directOnly = def.directOnly;
- if (typeof directOnly !== 'boolean') {
- throw new TypeError(`Virtual table module "${moduleName}" ${verb} a table definition with an invalid "directOnly" property (should be a boolean)`);
- }
- }
- // Generate SQL for the virtual table definition
- const columnDefinitions = [
- ...parameters.map(identifier).map(str => `${str} HIDDEN`),
- ...columns.map(identifier),
- ];
- return [
- `CREATE TABLE x(${columnDefinitions.join(', ')});`,
- wrapGenerator(rows, new Map(columns.map((x, i) => [x, parameters.length + i])), moduleName),
- parameters,
- safeIntegers,
- directOnly,
- ];
- }
- function wrapGenerator(generator, columnMap, moduleName) {
- return function* virtualTable(...args) {
- /*
- We must defensively clone any buffers in the arguments, because
- otherwise the generator could mutate one of them, which would cause
- us to return incorrect values for hidden columns, potentially
- corrupting the database.
- */
- const output = args.map(x => Buffer.isBuffer(x) ? Buffer.from(x) : x);
- for (let i = 0; i < columnMap.size; ++i) {
- output.push(null); // Fill with nulls to prevent gaps in array (v8 optimization)
- }
- for (const row of generator(...args)) {
- if (Array.isArray(row)) {
- extractRowArray(row, output, columnMap.size, moduleName);
- yield output;
- } else if (typeof row === 'object' && row !== null) {
- extractRowObject(row, output, columnMap, moduleName);
- yield output;
- } else {
- throw new TypeError(`Virtual table module "${moduleName}" yielded something that isn't a valid row object`);
- }
- }
- };
- }
- function extractRowArray(row, output, columnCount, moduleName) {
- if (row.length !== columnCount) {
- throw new TypeError(`Virtual table module "${moduleName}" yielded a row with an incorrect number of columns`);
- }
- const offset = output.length - columnCount;
- for (let i = 0; i < columnCount; ++i) {
- output[i + offset] = row[i];
- }
- }
- function extractRowObject(row, output, columnMap, moduleName) {
- let count = 0;
- for (const key of Object.keys(row)) {
- const index = columnMap.get(key);
- if (index === undefined) {
- throw new TypeError(`Virtual table module "${moduleName}" yielded a row with an undeclared column "${key}"`);
- }
- output[index] = row[key];
- count += 1;
- }
- if (count !== columnMap.size) {
- throw new TypeError(`Virtual table module "${moduleName}" yielded a row with missing columns`);
- }
- }
- function inferParameters({ length }) {
- if (!Number.isInteger(length) || length < 0) {
- throw new TypeError('Expected function.length to be a positive integer');
- }
- const params = [];
- for (let i = 0; i < length; ++i) {
- params.push(`$${i + 1}`);
- }
- return params;
- }
- const { hasOwnProperty } = Object.prototype;
- const { apply } = Function.prototype;
- const GeneratorFunctionPrototype = Object.getPrototypeOf(function*(){});
- const identifier = str => `"${str.replace(/"/g, '""')}"`;
- const defer = x => () => x;
|