├── CHANGELOG.md ├── .gitattributes ├── examples ├── pets_description.txt ├── constants.json ├── default_value.js ├── other_collections.yml ├── insomnia │ ├── gzip_request │ ├── randomlogo.png │ └── utf16_example ├── owners.js ├── math_func.js ├── test_plugin.js ├── pet_skills.yml └── main.js ├── eslint.config.js ├── prettier.config.js ├── src ├── utils │ ├── functional │ │ ├── identity.js │ │ ├── includes.js │ │ ├── reverse.js │ │ ├── difference.js │ │ ├── result.js │ │ ├── key_by.js │ │ ├── invert.js │ │ ├── validate.js │ │ ├── type.js │ │ ├── intersection.js │ │ ├── promise.js │ │ ├── values.js │ │ ├── once.js │ │ ├── uniq.js │ │ ├── merge.js │ │ ├── immutable.js │ │ ├── group.js │ │ └── reduce.js │ ├── transtype.js │ ├── template.js │ ├── errors.js │ ├── base64.js │ ├── string.js │ └── sums.js ├── patch │ ├── operators │ │ ├── generic.js │ │ ├── boolean.js │ │ └── main.js │ ├── validate │ │ ├── post_validators.js │ │ └── types │ │ │ ├── empty.js │ │ │ └── available.js │ ├── error.js │ ├── parse.js │ └── ref.js ├── databases │ ├── adapters │ │ ├── memory │ │ │ ├── defaults.js │ │ │ ├── query │ │ │ │ ├── find │ │ │ │ │ ├── limit.js │ │ │ │ │ ├── offset.js │ │ │ │ │ ├── order.js │ │ │ │ │ └── main.js │ │ │ │ ├── delete.js │ │ │ │ ├── upsert.js │ │ │ │ └── main.js │ │ │ ├── opts.js │ │ │ ├── features.js │ │ │ ├── connect.js │ │ │ ├── main.js │ │ │ └── disconnect.js │ │ ├── mongodb │ │ │ ├── disconnect.js │ │ │ ├── defaults.js │ │ │ ├── query │ │ │ │ ├── find │ │ │ │ │ ├── limit.js │ │ │ │ │ ├── offset.js │ │ │ │ │ ├── order.js │ │ │ │ │ └── main.js │ │ │ │ ├── delete.js │ │ │ │ └── upsert.js │ │ │ ├── features.js │ │ │ ├── main.js │ │ │ └── opts.js │ │ └── main.js │ ├── info.js │ ├── get.js │ ├── wrap.js │ └── features │ │ ├── generic.js │ │ └── filter.js ├── protocols │ ├── adapters │ │ ├── http │ │ │ ├── defaults.js │ │ │ ├── path.js │ │ │ ├── stop.js │ │ │ ├── query_string.js │ │ │ ├── content_negotiation │ │ │ │ ├── charset.js │ │ │ │ ├── content_type.js │ │ │ │ └── compress.js │ │ │ ├── opts.js │ │ │ ├── ip.js │ │ │ ├── method.js │ │ │ ├── headers.js │ │ │ └── main.js │ │ └── main.js │ ├── request │ │ ├── input.js │ │ ├── ip.js │ │ ├── path.js │ │ ├── headers.js │ │ ├── queryvars.js │ │ ├── method.js │ │ ├── content_negotiation │ │ │ ├── charset.js │ │ │ └── format.js │ │ └── origin.js │ ├── get.js │ ├── info.js │ └── wrap.js ├── middleware │ ├── protocol │ │ ├── rpc.js │ │ ├── parse.js │ │ └── requestid.js │ ├── rpc │ │ ├── method_check.js │ │ ├── router.js │ │ ├── actions.js │ │ └── parse.js │ ├── database │ │ ├── pick_adapter.js │ │ ├── rename_ids │ │ │ ├── order.js │ │ │ ├── data.js │ │ │ └── filter.js │ │ └── response.js │ ├── action │ │ ├── resolve.js │ │ ├── parse_response.js │ │ ├── add_actions │ │ │ └── values.js │ │ ├── data_arg │ │ │ └── data_path.js │ │ ├── assemble.js │ │ ├── rename_args.js │ │ ├── sort.js │ │ ├── modelscount.js │ │ ├── server_params.js │ │ ├── unknown_attrs │ │ │ └── all.js │ │ ├── rollback │ │ │ └── failure.js │ │ └── current_data │ │ │ └── main.js │ ├── sequencer │ │ ├── metadata.js │ │ ├── write │ │ │ └── current_data.js │ │ └── read │ │ │ ├── input.js │ │ │ └── paginate │ │ │ └── attr.js │ ├── request_response │ │ ├── pagination │ │ │ ├── encoding │ │ │ │ ├── convert_undefined.js │ │ │ │ ├── minify_names.js │ │ │ │ └── main.js │ │ │ └── output │ │ │ │ └── main.js │ │ ├── empty_models.js │ │ ├── duplicate_read.js │ │ ├── features.js │ │ ├── normalize_empty.js │ │ ├── aliases │ │ │ ├── order.js │ │ │ ├── response.js │ │ │ └── output.js │ │ ├── create_ids.js │ │ ├── authorize │ │ │ └── data.js │ │ ├── response_validation.js │ │ └── data_validation.js │ ├── time │ │ └── timestamp.js │ ├── final │ │ ├── perf_event.js │ │ ├── send_response │ │ │ ├── params.js │ │ │ ├── error.js │ │ │ ├── validate.js │ │ │ ├── serialize.js │ │ │ └── compress.js │ │ ├── status.js │ │ ├── duration.js │ │ └── call_event.js │ └── error │ │ └── handler.js ├── errors │ ├── reasons │ │ ├── engine.js │ │ ├── unknown.js │ │ ├── aborted.js │ │ ├── success.js │ │ ├── no_content_length.js │ │ ├── config_runtime.js │ │ ├── payload_limit.js │ │ ├── command.js │ │ ├── route.js │ │ ├── log.js │ │ ├── rpc.js │ │ ├── url_limit.js │ │ ├── format.js │ │ ├── charset.js │ │ ├── compress.js │ │ ├── database.js │ │ ├── protocol.js │ │ ├── validation.js │ │ ├── timeout.js │ │ ├── plugin.js │ │ ├── not_found.js │ │ ├── config_validation.js │ │ ├── conflict.js │ │ ├── authorization.js │ │ ├── method.js │ │ └── message.js │ └── instruction.js ├── snapshots │ └── build │ │ └── src │ │ ├── main.test.js.snap │ │ └── main.test.js.md ├── log │ ├── constants.js │ ├── info.js │ ├── adapters │ │ ├── custom │ │ │ ├── opts.js │ │ │ ├── report.js │ │ │ └── main.js │ │ ├── debug │ │ │ ├── report_perf.js │ │ │ ├── report.js │ │ │ └── main.js │ │ ├── main.js │ │ ├── console │ │ │ ├── main.js │ │ │ └── report.js │ │ └── http │ │ │ ├── main.js │ │ │ ├── get_opts.js │ │ │ └── opts.js │ ├── get.js │ ├── wrap.js │ ├── perf.js │ └── params.js ├── rpc │ ├── info.js │ ├── adapters │ │ ├── graphiql │ │ │ ├── main.js │ │ │ └── parse │ │ │ │ └── main.js │ │ ├── graphql │ │ │ ├── load │ │ │ │ ├── types │ │ │ │ │ ├── object │ │ │ │ │ │ ├── command.js │ │ │ │ │ │ ├── final_fields │ │ │ │ │ │ │ ├── args │ │ │ │ │ │ │ │ ├── params.js │ │ │ │ │ │ │ │ ├── silent.js │ │ │ │ │ │ │ │ ├── filter.js │ │ │ │ │ │ │ │ ├── dryrun.js │ │ │ │ │ │ │ │ ├── id.js │ │ │ │ │ │ │ │ ├── cascade.js │ │ │ │ │ │ │ │ └── order.js │ │ │ │ │ │ │ ├── main.js │ │ │ │ │ │ │ └── default.js │ │ │ │ │ │ ├── filter.js │ │ │ │ │ │ ├── no_attributes.js │ │ │ │ │ │ └── nested_colls.js │ │ │ │ │ └── array.js │ │ │ │ ├── main.js │ │ │ │ ├── type.js │ │ │ │ └── name.js │ │ │ ├── parse │ │ │ │ ├── definition │ │ │ │ │ ├── merge_select.js │ │ │ │ │ └── populate.js │ │ │ │ ├── fragments.js │ │ │ │ └── duplicates.js │ │ │ └── main.js │ │ ├── graphqlprint │ │ │ ├── main.js │ │ │ └── parse │ │ │ │ ├── main.js │ │ │ │ └── print.mustache │ │ ├── rest │ │ │ ├── main.js │ │ │ └── parse.js │ │ ├── jsonrpc │ │ │ └── main.js │ │ └── main.js │ ├── get.js │ ├── wrap.js │ ├── method_check.js │ └── transform.js ├── main.js ├── commands │ └── helpers.js ├── formats │ ├── extensions.js │ ├── charset.js │ ├── info.js │ ├── adapters │ │ ├── json.js │ │ ├── main.js │ │ ├── json5.js │ │ ├── hjson.js │ │ ├── raw.js │ │ └── ini.js │ ├── wrap.js │ ├── content.js │ └── file.js ├── serverinfo │ ├── process.js │ ├── versions.js │ └── main.js ├── compress │ ├── adapters │ │ ├── identity.js │ │ ├── main.js │ │ ├── gzip.js │ │ ├── deflate.js │ │ └── brotli.js │ ├── wrap.js │ ├── info.js │ └── get.js ├── validation │ ├── compile.js │ └── validator.js ├── config │ └── reducers │ │ ├── shortcuts │ │ ├── value.js │ │ ├── readonly.js │ │ ├── aliases.js │ │ ├── user_defaults.js │ │ ├── colls_names.js │ │ └── main.js │ │ ├── collname.js │ │ ├── protocols.js │ │ ├── log_validation.js │ │ ├── required_id.js │ │ ├── rpc.js │ │ ├── nested_coll.js │ │ ├── load │ │ └── main.js │ │ ├── type_validation.js │ │ ├── type.js │ │ └── colls_default.js ├── charsets │ ├── transform.js │ └── validate.js ├── filter │ ├── parse │ │ └── optimize.js │ └── operators │ │ ├── eq_neq.js │ │ ├── in_nin.js │ │ └── lt_gt_lte_gte.js ├── json_refs │ ├── find.js │ ├── ref_path.js │ └── load.js ├── functions │ ├── params │ │ └── keys.js │ ├── test.js │ ├── run.js │ └── tokenize.js ├── run │ ├── exit │ │ ├── protocol_close.js │ │ ├── message.js │ │ ├── db_close.js │ │ ├── wrapper.js │ │ └── setup.js │ ├── error.js │ ├── perf.js │ └── main.js ├── perf │ └── sort.js ├── bin │ ├── instructions.js │ └── available.js └── adapters │ └── get.js ├── CONTRIBUTING.md ├── ava.config.js ├── gulpfile.js ├── .gitignore ├── .github └── workflows │ └── workflow.yml ├── docs ├── dev │ ├── README.md │ ├── ROADMAP.md │ ├── development.md │ └── coding_style.md ├── README.md ├── client │ ├── arguments │ │ ├── silent.md │ │ ├── dryrun.md │ │ ├── sorting.md │ │ ├── selecting.md │ │ ├── params.md │ │ ├── renaming.md │ │ └── README.md │ ├── request │ │ └── README.md │ └── protocols │ │ └── README.md └── server │ ├── plugins │ ├── timestamp.md │ └── author.md │ ├── protocols │ ├── http.md │ └── README.md │ ├── data_model │ ├── compatibility.md │ ├── default.md │ └── relations.md │ ├── usage │ ├── error.md │ └── README.md │ ├── quality │ ├── README.md │ ├── documentation.md │ └── limits.md │ ├── databases │ ├── mongodb.md │ ├── memorydb.md │ └── README.md │ └── configuration │ ├── references.md │ └── formats.md ├── .editorconfig └── .all-contributorsrc /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /examples/pets_description.txt: -------------------------------------------------------------------------------- 1 | Cute animal -------------------------------------------------------------------------------- /examples/constants.json: -------------------------------------------------------------------------------- 1 | { 2 | "TestConstant": 10 3 | } 4 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | export { default } from '@ehmicky/eslint-config' 2 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | export { default } from '@ehmicky/prettier-config' 2 | -------------------------------------------------------------------------------- /src/utils/functional/identity.js: -------------------------------------------------------------------------------- 1 | export const identity = (val) => val 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | See the [developer's documentation](docs/dev/README.md). 2 | -------------------------------------------------------------------------------- /ava.config.js: -------------------------------------------------------------------------------- 1 | export { default } from '@ehmicky/dev-tasks/ava.config.js' 2 | -------------------------------------------------------------------------------- /examples/default_value.js: -------------------------------------------------------------------------------- 1 | export default ({ command }) => `happy ${command}` 2 | -------------------------------------------------------------------------------- /src/patch/operators/generic.js: -------------------------------------------------------------------------------- 1 | export const set = { 2 | apply: ({ arg: opVal }) => opVal, 3 | } 4 | -------------------------------------------------------------------------------- /examples/other_collections.yml: -------------------------------------------------------------------------------- 1 | owners: 2 | $ref: owners.js 3 | pet_skills: 4 | $ref: pet_skills.yml 5 | -------------------------------------------------------------------------------- /src/databases/adapters/memory/defaults.js: -------------------------------------------------------------------------------- 1 | export const defaults = { 2 | save: false, 3 | data: {}, 4 | } 5 | -------------------------------------------------------------------------------- /examples/insomnia/gzip_request: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ehmicky/autoserver/HEAD/examples/insomnia/gzip_request -------------------------------------------------------------------------------- /examples/insomnia/randomlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ehmicky/autoserver/HEAD/examples/insomnia/randomlogo.png -------------------------------------------------------------------------------- /src/protocols/adapters/http/defaults.js: -------------------------------------------------------------------------------- 1 | export const defaults = { 2 | hostname: 'localhost', 3 | port: 80, 4 | } 5 | -------------------------------------------------------------------------------- /src/protocols/adapters/main.js: -------------------------------------------------------------------------------- 1 | import { http } from './http/main.js' 2 | 3 | export const PROTOCOL_ADAPTERS = [http] 4 | -------------------------------------------------------------------------------- /src/middleware/protocol/rpc.js: -------------------------------------------------------------------------------- 1 | // Fires rpc layer 2 | export const fireRpc = (mInput, nextLayer) => nextLayer(mInput, 'rpc') 3 | -------------------------------------------------------------------------------- /src/databases/adapters/mongodb/disconnect.js: -------------------------------------------------------------------------------- 1 | // Stops connection 2 | export const disconnect = ({ connection: db }) => db.close() 3 | -------------------------------------------------------------------------------- /src/errors/reasons/engine.js: -------------------------------------------------------------------------------- 1 | export const ENGINE = { 2 | status: 'SERVER_ERROR', 3 | title: 'Internal engine error', 4 | } 5 | -------------------------------------------------------------------------------- /src/errors/reasons/unknown.js: -------------------------------------------------------------------------------- 1 | export const UNKNOWN = { 2 | status: 'SERVER_ERROR', 3 | title: 'Internal uncaught error', 4 | } 5 | -------------------------------------------------------------------------------- /src/snapshots/build/src/main.test.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ehmicky/autoserver/HEAD/src/snapshots/build/src/main.test.js.snap -------------------------------------------------------------------------------- /src/log/constants.js: -------------------------------------------------------------------------------- 1 | export const LEVELS = ['info', 'log', 'warn', 'error'] 2 | 3 | export const DEFAULT_LOGGER = { provider: 'console' } 4 | -------------------------------------------------------------------------------- /src/errors/reasons/aborted.js: -------------------------------------------------------------------------------- 1 | export const ABORTED = { 2 | status: 'CLIENT_ERROR', 3 | title: 'The request was aborted by the client', 4 | } 5 | -------------------------------------------------------------------------------- /examples/insomnia/utf16_example: -------------------------------------------------------------------------------- 1 | { " i d " : " 7 " , " n a m e " : " J S O N " , " p h o t o _ u r l s " : [ " u r l A " ] , " f r i e n d s " : [ ] } -------------------------------------------------------------------------------- /src/errors/reasons/success.js: -------------------------------------------------------------------------------- 1 | export const SUCCESS = { 2 | status: 'SUCCESS', 3 | title: 'Request was successful, i.e. there is no error', 4 | } 5 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | import '@ehmicky/dev-tasks/register.js' 2 | 3 | export * from '@ehmicky/dev-tasks' 4 | 5 | export { runDebug, runDev, runProd } from './gulp/run.js' 6 | -------------------------------------------------------------------------------- /src/databases/adapters/mongodb/defaults.js: -------------------------------------------------------------------------------- 1 | export const defaults = { 2 | hostname: 'localhost', 3 | port: 27_017, 4 | dbname: 'data', 5 | opts: {}, 6 | } 7 | -------------------------------------------------------------------------------- /src/protocols/request/input.js: -------------------------------------------------------------------------------- 1 | export const parseInput = ({ 2 | protocolAdapter: { getInput }, 3 | specific, 4 | method, 5 | }) => getInput({ specific, method }) 6 | -------------------------------------------------------------------------------- /src/errors/reasons/no_content_length.js: -------------------------------------------------------------------------------- 1 | export const NO_CONTENT_LENGTH = { 2 | status: 'CLIENT_ERROR', 3 | title: "The request payload's length must be specified", 4 | } 5 | -------------------------------------------------------------------------------- /src/rpc/info.js: -------------------------------------------------------------------------------- 1 | import { getNames } from '../adapters/get.js' 2 | 3 | import { RPC_ADAPTERS } from './adapters/main.js' 4 | 5 | export const RPCS = getNames(RPC_ADAPTERS) 6 | -------------------------------------------------------------------------------- /src/databases/adapters/main.js: -------------------------------------------------------------------------------- 1 | import { memory } from './memory/main.js' 2 | import { mongodb } from './mongodb/main.js' 3 | 4 | export const DATABASE_ADAPTERS = [memory, mongodb] 5 | -------------------------------------------------------------------------------- /src/patch/operators/boolean.js: -------------------------------------------------------------------------------- 1 | export const invert = { 2 | attribute: ['boolean'], 3 | 4 | argument: ['empty'], 5 | 6 | apply: ({ value: attrVal = false }) => !attrVal, 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/functional/includes.js: -------------------------------------------------------------------------------- 1 | import { isDeepStrictEqual } from 'node:util' 2 | 3 | export const includes = (arr, valA) => 4 | arr.some((valB) => isDeepStrictEqual(valA, valB)) 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.swp 3 | npm-debug.log 4 | node_modules 5 | /core 6 | .eslintcache 7 | .lycheecache 8 | .npmrc 9 | .yarn-error.log 10 | !.github/ 11 | /coverage 12 | /build 13 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [push, pull_request] 3 | jobs: 4 | combinations: 5 | uses: ehmicky/dev-tasks/.github/workflows/build.yml@main 6 | secrets: inherit 7 | -------------------------------------------------------------------------------- /src/log/info.js: -------------------------------------------------------------------------------- 1 | import { getMember } from '../adapters/get.js' 2 | 3 | import { LOG_ADAPTERS } from './adapters/main.js' 4 | 5 | export const LOG_OPTS = getMember(LOG_ADAPTERS, 'opts', {}) 6 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { wrapInstruction } from './errors/instruction.js' 2 | import { run as runInstruction } from './run/main.js' 3 | 4 | export const run = wrapInstruction('run', runInstruction) 5 | -------------------------------------------------------------------------------- /src/protocols/adapters/http/path.js: -------------------------------------------------------------------------------- 1 | // Retrieves path without query string nor hash 2 | export const getPath = ({ 3 | specific: { 4 | req: { url }, 5 | }, 6 | }) => url.replace(/[?#].*/u, '') 7 | -------------------------------------------------------------------------------- /src/protocols/adapters/http/stop.js: -------------------------------------------------------------------------------- 1 | import { promisify } from 'node:util' 2 | 3 | // Try a graceful server exit 4 | export const stopServer = ({ server }) => promisify(server.close.bind(server))() 5 | -------------------------------------------------------------------------------- /src/middleware/rpc/method_check.js: -------------------------------------------------------------------------------- 1 | // Check if protocol method is allowed for current rpc 2 | export const methodCheck = ({ rpcAdapter: { checkMethod }, method }) => { 3 | checkMethod({ method }) 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/functional/reverse.js: -------------------------------------------------------------------------------- 1 | // Like array.reverse() but does not mutate argument 2 | // eslint-disable-next-line fp/no-mutating-methods 3 | export const reverseArray = (array) => [...array].reverse() 4 | -------------------------------------------------------------------------------- /src/utils/functional/difference.js: -------------------------------------------------------------------------------- 1 | import { includes } from './includes.js' 2 | 3 | // Like Lodash difference() 4 | export const difference = (arrA, arrB) => 5 | arrA.filter((val) => !includes(arrB, val)) 6 | -------------------------------------------------------------------------------- /src/commands/helpers.js: -------------------------------------------------------------------------------- 1 | // Merge each action `commandpath` into a comma-separated list 2 | export const mergeCommandpaths = ({ actions }) => 3 | actions.map(({ commandpath }) => commandpath.join('.')).join(', ') 4 | -------------------------------------------------------------------------------- /src/middleware/database/pick_adapter.js: -------------------------------------------------------------------------------- 1 | // Pick database adapter 2 | export const pickDatabaseAdapter = ({ dbAdapters, collname }) => { 3 | const dbAdapter = dbAdapters[collname] 4 | return { dbAdapter } 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/functional/result.js: -------------------------------------------------------------------------------- 1 | // Like Lodash result(), but faster 2 | export const result = (val, ...args) => { 3 | if (typeof val !== 'function') { 4 | return val 5 | } 6 | 7 | return val(...args) 8 | } 9 | -------------------------------------------------------------------------------- /docs/dev/README.md: -------------------------------------------------------------------------------- 1 | # Development guide (table of contents) 2 | 3 | - [How to start the dev environment](development.md) 4 | - [Coding style](coding_style.md) 5 | - [Terminology](terminology.md) 6 | - [Roadmap](ROADMAP.md) 7 | -------------------------------------------------------------------------------- /examples/owners.js: -------------------------------------------------------------------------------- 1 | const owners = { 2 | description: "A pet's owner", 3 | attributes: { 4 | name: {}, 5 | pets: { 6 | type: 'pets[]', 7 | }, 8 | }, 9 | } 10 | 11 | export default owners 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | max_line_length = 80 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /examples/math_func.js: -------------------------------------------------------------------------------- 1 | export default ({ ip }, { numA, numB, numC, numD }) => { 2 | const ipNumber = Number(ip.slice(0, IP_NUMBER_LENGTH)) 3 | return ipNumber + numA * numB + numC * numD 4 | } 5 | 6 | const IP_NUMBER_LENGTH = 3 7 | -------------------------------------------------------------------------------- /src/databases/adapters/memory/query/find/limit.js: -------------------------------------------------------------------------------- 1 | // Pagination limiting 2 | export const limitResponse = ({ data, limit }) => { 3 | if (limit === undefined) { 4 | return data 5 | } 6 | 7 | return data.slice(0, limit) 8 | } 9 | -------------------------------------------------------------------------------- /src/formats/extensions.js: -------------------------------------------------------------------------------- 1 | // Retrieve format's prefered extension 2 | export const getExtension = ({ extensions: [extension] = [] }) => { 3 | if (extension === undefined) { 4 | return 5 | } 6 | 7 | return `.${extension}` 8 | } 9 | -------------------------------------------------------------------------------- /src/rpc/adapters/graphiql/main.js: -------------------------------------------------------------------------------- 1 | import { parse } from './parse/main.js' 2 | 3 | export const graphiql = { 4 | name: 'graphiql', 5 | title: 'GraphiQL', 6 | methods: ['GET'], 7 | routes: ['/graphiql'], 8 | parse, 9 | } 10 | -------------------------------------------------------------------------------- /src/rpc/adapters/graphql/load/types/object/command.js: -------------------------------------------------------------------------------- 1 | // Add command information to `def` 2 | export const addCommand = (def, { parentDef }) => { 3 | const command = def.command || parentDef.command 4 | return { ...def, command } 5 | } 6 | -------------------------------------------------------------------------------- /src/databases/adapters/mongodb/query/find/limit.js: -------------------------------------------------------------------------------- 1 | // Apply `args.pagesize` 2 | export const limitResponse = ({ cursor, limit }) => { 3 | if (limit === undefined) { 4 | return cursor 5 | } 6 | 7 | return cursor.limit(limit) 8 | } 9 | -------------------------------------------------------------------------------- /src/databases/adapters/mongodb/query/find/offset.js: -------------------------------------------------------------------------------- 1 | // Apply `args.page` 2 | export const offsetResponse = ({ cursor, offset }) => { 3 | if (offset === undefined) { 4 | return cursor 5 | } 6 | 7 | return cursor.offset(offset) 8 | } 9 | -------------------------------------------------------------------------------- /src/errors/reasons/config_runtime.js: -------------------------------------------------------------------------------- 1 | // Extra: 2 | // - path 'VARR' 3 | // - value VAL 4 | // - suggestions VAL_ARR 5 | export const CONFIG_RUNTIME = { 6 | status: 'SERVER_ERROR', 7 | title: 'Wrong configuration caught runtime', 8 | } 9 | -------------------------------------------------------------------------------- /src/log/adapters/custom/opts.js: -------------------------------------------------------------------------------- 1 | export const opts = { 2 | type: 'object', 3 | additionalProperties: false, 4 | required: ['report'], 5 | properties: { 6 | report: { 7 | typeof: 'function', 8 | }, 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /src/rpc/get.js: -------------------------------------------------------------------------------- 1 | import { getAdapter } from '../adapters/get.js' 2 | 3 | import { rpcAdapters } from './wrap.js' 4 | 5 | // Retrieves rpc adapter 6 | export const getRpc = (key) => 7 | getAdapter({ adapters: rpcAdapters, key, name: 'RPC' }) 8 | -------------------------------------------------------------------------------- /src/log/adapters/custom/report.js: -------------------------------------------------------------------------------- 1 | import { runConfigFunc } from '../../../functions/run.js' 2 | 3 | // Report log 4 | export const report = ({ opts: { report: configFunc }, configFuncInput }) => 5 | runConfigFunc({ configFunc, ...configFuncInput }) 6 | -------------------------------------------------------------------------------- /src/log/get.js: -------------------------------------------------------------------------------- 1 | import { getAdapter } from '../adapters/get.js' 2 | 3 | import { logAdapters } from './wrap.js' 4 | 5 | // Retrieves log adapter 6 | export const getLog = (key) => 7 | getAdapter({ adapters: logAdapters, key, name: 'log provider' }) 8 | -------------------------------------------------------------------------------- /src/patch/validate/post_validators.js: -------------------------------------------------------------------------------- 1 | import { applyCheck } from './check.js' 2 | import { checkEmpty } from './types/empty.js' 3 | 4 | // Apply validation after model.ATTR has been resolved 5 | export const POST_VALIDATORS = [checkEmpty, applyCheck] 6 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | You can learn: 4 | 5 | - how to perform client requests against the server [here](client/README.md) 6 | - how to run the server [here](server/README.md) 7 | - how to contribute to the code base [here](dev/README.md) 8 | -------------------------------------------------------------------------------- /src/serverinfo/process.js: -------------------------------------------------------------------------------- 1 | import { pid } from 'node:process' 2 | 3 | // Retrieve process-specific information 4 | export const getProcessInfo = ({ host, processName }) => { 5 | const name = processName || host.name 6 | 7 | return { id: pid, name } 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/functional/key_by.js: -------------------------------------------------------------------------------- 1 | // Similar to Lodash keyBy() but faster 2 | export const keyBy = (array, attr = 'name') => { 3 | const objs = array.map((obj) => ({ [obj[attr]]: obj })) 4 | const objA = Object.assign({}, ...objs) 5 | return objA 6 | } 7 | -------------------------------------------------------------------------------- /src/errors/reasons/payload_limit.js: -------------------------------------------------------------------------------- 1 | // Extra: 2 | // - kind 'size|models|commands|depth' 3 | // - value NUM 4 | // - limit NUM 5 | export const PAYLOAD_LIMIT = { 6 | status: 'CLIENT_ERROR', 7 | title: 'The request or response payload is too large', 8 | } 9 | -------------------------------------------------------------------------------- /src/log/adapters/debug/report_perf.js: -------------------------------------------------------------------------------- 1 | // Report performance by printing it on console 2 | export const reportPerf = ({ log: { measuresmessage } }) => { 3 | // eslint-disable-next-line no-console, no-restricted-globals 4 | console.log(measuresmessage) 5 | } 6 | -------------------------------------------------------------------------------- /src/rpc/adapters/graphqlprint/main.js: -------------------------------------------------------------------------------- 1 | import { parse } from './parse/main.js' 2 | 3 | export const graphqlprint = { 4 | name: 'graphqlprint', 5 | title: 'GraphQLPrint', 6 | routes: ['/graphql/schema'], 7 | methods: ['GET'], 8 | parse, 9 | } 10 | -------------------------------------------------------------------------------- /src/errors/reasons/command.js: -------------------------------------------------------------------------------- 1 | // Extra: 2 | // - value STR 3 | // - suggestions STR_ARR 4 | export const COMMAND = { 5 | status: 'CLIENT_ERROR', 6 | title: 'The command name is invalid', 7 | getMessage: ({ value }) => `Unsupported command '${value}'`, 8 | } 9 | -------------------------------------------------------------------------------- /src/log/adapters/debug/report.js: -------------------------------------------------------------------------------- 1 | // Report log by printing it on console 2 | export const report = ({ log }) => { 3 | const logA = JSON.stringify(log, undefined, 2) 4 | 5 | // eslint-disable-next-line no-console, no-restricted-globals 6 | console.log(logA) 7 | } 8 | -------------------------------------------------------------------------------- /src/middleware/protocol/parse.js: -------------------------------------------------------------------------------- 1 | // Retrieves protocol request's input 2 | // TODO: remove specific 3 | export const parseProtocol = ( 4 | { protocolAdapter: { parseRequest }, config }, 5 | nextLayer, 6 | { measures }, 7 | ) => parseRequest({ config, measures }) 8 | -------------------------------------------------------------------------------- /src/compress/adapters/identity.js: -------------------------------------------------------------------------------- 1 | // No compression 2 | const compress = (content) => content 3 | 4 | const decompress = (content) => content 5 | 6 | export const identity = { 7 | name: 'identity', 8 | title: 'None', 9 | compress, 10 | decompress, 11 | } 12 | -------------------------------------------------------------------------------- /src/databases/adapters/memory/opts.js: -------------------------------------------------------------------------------- 1 | export const opts = { 2 | type: 'object', 3 | additionalProperties: false, 4 | properties: { 5 | data: { 6 | type: 'object', 7 | }, 8 | save: { 9 | type: 'boolean', 10 | }, 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /src/errors/reasons/route.js: -------------------------------------------------------------------------------- 1 | // Extra: 2 | // - value 'PATH' 3 | // - suggestions VAL_ARR 4 | export const ROUTE = { 5 | status: 'CLIENT_ERROR', 6 | title: 'The URL or route is invalid', 7 | getMessage: ({ value }) => `The URL or route '${value}' is invalid`, 8 | } 9 | -------------------------------------------------------------------------------- /src/protocols/get.js: -------------------------------------------------------------------------------- 1 | import { getAdapter } from '../adapters/get.js' 2 | 3 | import { protocolAdapters } from './wrap.js' 4 | 5 | // Retrieves protocol adapter 6 | export const getProtocol = (key) => 7 | getAdapter({ adapters: protocolAdapters, key, name: 'protocol' }) 8 | -------------------------------------------------------------------------------- /examples/test_plugin.js: -------------------------------------------------------------------------------- 1 | import { nextTick } from 'node:process' 2 | import { promisify } from 'node:util' 3 | 4 | export default async ({ config, opts: { example_option: opt } }) => { 5 | await promisify(nextTick)() 6 | 7 | return { ...config, $plugin_attr: opt } 8 | } 9 | -------------------------------------------------------------------------------- /src/protocols/adapters/http/query_string.js: -------------------------------------------------------------------------------- 1 | // Retrieves query string from a URL 2 | export const getQueryString = ({ 3 | specific: { 4 | req: { url }, 5 | }, 6 | }) => { 7 | const { search = '' } = new URL(`http://localhost/${url}`) 8 | return search 9 | } 10 | -------------------------------------------------------------------------------- /src/rpc/adapters/rest/main.js: -------------------------------------------------------------------------------- 1 | import { parse } from './parse.js' 2 | 3 | export const rest = { 4 | name: 'rest', 5 | title: 'REST', 6 | routes: ['/rest/:clientCollname{/:id}'], 7 | methods: ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE'], 8 | parse, 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/functional/invert.js: -------------------------------------------------------------------------------- 1 | // Similar to Lodash _.invert(), but with plain JavaScript 2 | export const invert = (obj) => { 3 | const objs = Object.entries(obj).map(([key, value]) => ({ [value]: key })) 4 | const objA = Object.assign({}, ...objs) 5 | return objA 6 | } 7 | -------------------------------------------------------------------------------- /src/middleware/action/resolve.js: -------------------------------------------------------------------------------- 1 | // Fire all read or write actions, retrieving some `results` 2 | export const resolveActions = ({ top: { command }, mInput }, nextLayer) => { 3 | const layerName = command.type === 'find' ? 'read' : 'write' 4 | return nextLayer(mInput, layerName) 5 | } 6 | -------------------------------------------------------------------------------- /src/databases/info.js: -------------------------------------------------------------------------------- 1 | import { getMember } from '../adapters/get.js' 2 | 3 | import { DATABASE_ADAPTERS } from './adapters/main.js' 4 | 5 | export const DATABASE_OPTS = getMember(DATABASE_ADAPTERS, 'opts', {}) 6 | export const DATABASE_DEFAULTS = getMember(DATABASE_ADAPTERS, 'defaults', {}) 7 | -------------------------------------------------------------------------------- /src/log/adapters/main.js: -------------------------------------------------------------------------------- 1 | import { logConsole } from './console/main.js' 2 | import { logCustom } from './custom/main.js' 3 | import { logDebug } from './debug/main.js' 4 | import { logHttp } from './http/main.js' 5 | 6 | export const LOG_ADAPTERS = [logConsole, logDebug, logHttp, logCustom] 7 | -------------------------------------------------------------------------------- /src/databases/adapters/memory/query/find/offset.js: -------------------------------------------------------------------------------- 1 | // Pagination offsetting 2 | // If offset is too big, just return empty array 3 | export const offsetResponse = ({ data, offset }) => { 4 | if (offset === undefined) { 5 | return data 6 | } 7 | 8 | return data.slice(offset) 9 | } 10 | -------------------------------------------------------------------------------- /src/formats/charset.js: -------------------------------------------------------------------------------- 1 | // Retrieves format's prefered charset 2 | export const getCharset = ({ charsets: [charset] = [] }) => charset 3 | 4 | // Checks if charset is supported by format 5 | export const hasCharset = ({ charsets }, charset) => 6 | charsets === undefined || charsets.includes(charset) 7 | -------------------------------------------------------------------------------- /src/errors/reasons/log.js: -------------------------------------------------------------------------------- 1 | import { getAdapterMessage } from './message.js' 2 | 3 | // Extra: 4 | // - adapter `{string}`: adapter name 5 | export const LOG = { 6 | status: 'SERVER_ERROR', 7 | title: 'Internal error related to a specific log adapter', 8 | getMessage: getAdapterMessage, 9 | } 10 | -------------------------------------------------------------------------------- /src/errors/reasons/rpc.js: -------------------------------------------------------------------------------- 1 | import { getAdapterMessage } from './message.js' 2 | 3 | // Extra: 4 | // - adapter `{string}`: adapter name 5 | export const RPC = { 6 | status: 'SERVER_ERROR', 7 | title: 'Internal error related to a specific rpc adapter', 8 | getMessage: getAdapterMessage, 9 | } 10 | -------------------------------------------------------------------------------- /src/errors/reasons/url_limit.js: -------------------------------------------------------------------------------- 1 | // Extra: 2 | // - value NUM 3 | // - limit NUM 4 | export const URL_LIMIT = { 5 | status: 'CLIENT_ERROR', 6 | title: 'The URL is too large', 7 | getMessage: ({ limit, value }) => 8 | `URL length must be less than ${limit} characters but has ${value}`, 9 | } 10 | -------------------------------------------------------------------------------- /src/protocols/adapters/http/content_negotiation/charset.js: -------------------------------------------------------------------------------- 1 | import { getContentType } from './content_type.js' 2 | 3 | // Use similar logic as `args.format`, but for `args.charset` 4 | export const getCharset = ({ specific }) => { 5 | const { charset } = getContentType({ specific }) 6 | return charset 7 | } 8 | -------------------------------------------------------------------------------- /src/errors/reasons/format.js: -------------------------------------------------------------------------------- 1 | import { getAdapterMessage } from './message.js' 2 | 3 | // Extra: 4 | // - adapter `{string}`: adapter name 5 | export const FORMAT = { 6 | status: 'SERVER_ERROR', 7 | title: 'Internal error related to a specific format adapter', 8 | getMessage: getAdapterMessage, 9 | } 10 | -------------------------------------------------------------------------------- /src/compress/adapters/main.js: -------------------------------------------------------------------------------- 1 | import { brotli } from './brotli.js' 2 | import { deflate } from './deflate.js' 3 | import { gzip } from './gzip.js' 4 | import { identity } from './identity.js' 5 | 6 | // Order matters, as first ones will have priority 7 | export const COMPRESS_ADAPTERS = [brotli, deflate, gzip, identity] 8 | -------------------------------------------------------------------------------- /src/databases/adapters/memory/query/find/order.js: -------------------------------------------------------------------------------- 1 | import { sortByAttributes } from '../../../../../utils/functional/sort.js' 2 | 3 | // `order` sorting 4 | export const sortResponse = ({ data, order }) => { 5 | if (!order) { 6 | return data 7 | } 8 | 9 | return sortByAttributes(data, order) 10 | } 11 | -------------------------------------------------------------------------------- /src/errors/reasons/charset.js: -------------------------------------------------------------------------------- 1 | import { getAdapterMessage } from './message.js' 2 | 3 | // Extra: 4 | // - adapter `{string}`: adapter name 5 | export const CHARSET = { 6 | status: 'SERVER_ERROR', 7 | title: 'Internal error related to a specific charset adapter', 8 | getMessage: getAdapterMessage, 9 | } 10 | -------------------------------------------------------------------------------- /src/errors/reasons/compress.js: -------------------------------------------------------------------------------- 1 | import { getAdapterMessage } from './message.js' 2 | 3 | // Extra: 4 | // - adapter `{string}`: adapter name 5 | export const COMPRESS = { 6 | status: 'SERVER_ERROR', 7 | title: 'Internal error related to a specific compress adapter', 8 | getMessage: getAdapterMessage, 9 | } 10 | -------------------------------------------------------------------------------- /src/errors/reasons/database.js: -------------------------------------------------------------------------------- 1 | import { getAdapterMessage } from './message.js' 2 | 3 | // Extra: 4 | // - adapter `{string}`: adapter name 5 | export const DATABASE = { 6 | status: 'SERVER_ERROR', 7 | title: 'Internal error related to a specific database adapter', 8 | getMessage: getAdapterMessage, 9 | } 10 | -------------------------------------------------------------------------------- /src/errors/reasons/protocol.js: -------------------------------------------------------------------------------- 1 | import { getAdapterMessage } from './message.js' 2 | 3 | // Extra: 4 | // - adapter `{string}`: adapter name 5 | export const PROTOCOL = { 6 | status: 'SERVER_ERROR', 7 | title: 'Internal error related to a specific protocol adapter', 8 | getMessage: getAdapterMessage, 9 | } 10 | -------------------------------------------------------------------------------- /src/protocols/adapters/http/opts.js: -------------------------------------------------------------------------------- 1 | export const opts = { 2 | type: 'object', 3 | additionalProperties: false, 4 | properties: { 5 | hostname: { 6 | type: 'string', 7 | }, 8 | port: { 9 | type: 'integer', 10 | minimum: 0, 11 | maximum: 65_535, 12 | }, 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/transtype.js: -------------------------------------------------------------------------------- 1 | // Tries to guess a value's type from its string serialized value 2 | // @param {string} string 3 | // @param {string|integer|float|boolean} value 4 | export const transtype = (string) => { 5 | try { 6 | return JSON.parse(string) 7 | } catch { 8 | return string 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/log/adapters/custom/main.js: -------------------------------------------------------------------------------- 1 | import { opts } from './opts.js' 2 | import { report } from './report.js' 3 | 4 | export const logCustom = { 5 | name: 'custom', 6 | title: 'Custom log handler', 7 | description: 'Log handler using a custom function', 8 | report, 9 | reportPerf: report, 10 | opts, 11 | } 12 | -------------------------------------------------------------------------------- /src/middleware/action/parse_response.js: -------------------------------------------------------------------------------- 1 | // Add content type, and remove top-level key 2 | // Also add metadata 3 | export const parseResponse = ({ response }) => { 4 | const type = Array.isArray(response) ? 'models' : 'model' 5 | 6 | const responseA = { content: response, type } 7 | return { response: responseA } 8 | } 9 | -------------------------------------------------------------------------------- /src/protocols/request/ip.js: -------------------------------------------------------------------------------- 1 | import { validateString } from './validate.js' 2 | 3 | export const parseIp = ({ 4 | protocolAdapter, 5 | protocolAdapter: { getIp }, 6 | specific, 7 | }) => { 8 | const ip = getIp({ specific }) 9 | 10 | validateString(ip, 'ip', protocolAdapter) 11 | 12 | return { ip } 13 | } 14 | -------------------------------------------------------------------------------- /src/validation/compile.js: -------------------------------------------------------------------------------- 1 | import { getCustomValidator } from './custom_validator.js' 2 | 3 | // Compile JSON schema 4 | export const compile = ({ config, jsonSchema }) => { 5 | const validator = getCustomValidator({ config }) 6 | const compiledJsonSchema = validator.compile(jsonSchema) 7 | return compiledJsonSchema 8 | } 9 | -------------------------------------------------------------------------------- /src/config/reducers/shortcuts/value.js: -------------------------------------------------------------------------------- 1 | import { getShortcut } from '../../helpers.js' 2 | 3 | // Gets a map of collections' `value` 4 | // e.g. { my_coll: { attrName: value, ... }, ... } 5 | export const valuesMap = ({ config }) => 6 | getShortcut({ config, filter: 'value', mapper }) 7 | 8 | const mapper = ({ value }) => value 9 | -------------------------------------------------------------------------------- /src/databases/get.js: -------------------------------------------------------------------------------- 1 | import { getAdapter } from '../adapters/get.js' 2 | 3 | import { databaseAdapters } from './wrap.js' 4 | 5 | // Retrieves database adapter 6 | export const getDatabase = (key) => 7 | getAdapter({ adapters: databaseAdapters, key, name: 'database' }) 8 | 9 | export const DEFAULT_DATABASE = 'memory' 10 | -------------------------------------------------------------------------------- /src/utils/template.js: -------------------------------------------------------------------------------- 1 | import { readFile } from 'node:fs/promises' 2 | 3 | import mustache from 'mustache' 4 | 5 | export const renderTemplate = async ({ template, data }) => { 6 | const htmlFile = await readFile(template, 'utf8') 7 | const htmlString = mustache.render(htmlFile, data) 8 | return htmlString 9 | } 10 | -------------------------------------------------------------------------------- /src/errors/reasons/validation.js: -------------------------------------------------------------------------------- 1 | // Extra: 2 | // - kind 'feature|protocol|rpc|argument|data|constraint' 3 | // - path 'VARR' 4 | // - value VAL 5 | // - model OBJ 6 | // - suggestions VAL_ARR 7 | export const VALIDATION = { 8 | status: 'CLIENT_ERROR', 9 | title: 'The request syntax or semantics is invalid', 10 | } 11 | -------------------------------------------------------------------------------- /src/log/adapters/debug/main.js: -------------------------------------------------------------------------------- 1 | import { report } from './report.js' 2 | import { reportPerf } from './report_perf.js' 3 | 4 | export const logDebug = { 5 | name: 'debug', 6 | title: 'Debug log handler', 7 | description: 'Log handler printing on the console, meant for debugging', 8 | report, 9 | reportPerf, 10 | } 11 | -------------------------------------------------------------------------------- /src/log/wrap.js: -------------------------------------------------------------------------------- 1 | import { wrapAdapters } from '../adapters/wrap.js' 2 | 3 | import { LOG_ADAPTERS } from './adapters/main.js' 4 | 5 | const members = ['name', 'title', 'report', 'reportPerf', 'getOpts'] 6 | 7 | export const logAdapters = wrapAdapters({ 8 | adapters: LOG_ADAPTERS, 9 | members, 10 | reason: 'LOG', 11 | }) 12 | -------------------------------------------------------------------------------- /src/utils/functional/validate.js: -------------------------------------------------------------------------------- 1 | import { throwError } from '../errors.js' 2 | 3 | import { isObject } from './type.js' 4 | 5 | export const checkObject = (obj) => { 6 | if (isObject(obj)) { 7 | return 8 | } 9 | 10 | const message = `Utility must be used with objects: '${obj}'` 11 | throwError(message) 12 | } 13 | -------------------------------------------------------------------------------- /docs/client/arguments/silent.md: -------------------------------------------------------------------------------- 1 | # Silent output 2 | 3 | Silent outputs can be requested using the `silent` 4 | [argument](../rpc/README.md#rpc) with any command. 5 | 6 | The response will be empty, unless an error occurred. The command will still be 7 | performed. 8 | 9 | ```HTTP 10 | DELETE /rest/users/1?silent=true 11 | ``` 12 | -------------------------------------------------------------------------------- /src/middleware/action/add_actions/values.js: -------------------------------------------------------------------------------- 1 | export const getValues = ({ actions, filter, mapper, ...rest }) => 2 | actions 3 | .filter(({ args }) => filterArgs({ args, filter })) 4 | .flatMap((action) => mapper({ action, ...rest })) 5 | 6 | const filterArgs = ({ args, filter }) => 7 | filter.some((key) => args[key] !== undefined) 8 | -------------------------------------------------------------------------------- /src/middleware/rpc/router.js: -------------------------------------------------------------------------------- 1 | import { getRpcByPath } from '../../rpc/router.js' 2 | 3 | // Add route and URL parameters to mInput 4 | export const router = ({ path }) => { 5 | const { 6 | rpcAdapter, 7 | rpcAdapter: { name: rpc }, 8 | pathvars, 9 | } = getRpcByPath(path) 10 | return { rpc, rpcAdapter, pathvars } 11 | } 12 | -------------------------------------------------------------------------------- /src/rpc/adapters/jsonrpc/main.js: -------------------------------------------------------------------------------- 1 | import { parse } from './parse.js' 2 | import { transformError, transformSuccess } from './response.js' 3 | 4 | export const jsonrpc = { 5 | name: 'jsonrpc', 6 | title: 'JSON-RPC', 7 | routes: ['/jsonrpc'], 8 | methods: ['POST'], 9 | parse, 10 | transformSuccess, 11 | transformError, 12 | } 13 | -------------------------------------------------------------------------------- /src/rpc/adapters/main.js: -------------------------------------------------------------------------------- 1 | import { graphiql } from './graphiql/main.js' 2 | import { graphql } from './graphql/main.js' 3 | import { graphqlprint } from './graphqlprint/main.js' 4 | import { jsonrpc } from './jsonrpc/main.js' 5 | import { rest } from './rest/main.js' 6 | 7 | export const RPC_ADAPTERS = [rest, graphql, graphiql, graphqlprint, jsonrpc] 8 | -------------------------------------------------------------------------------- /src/compress/wrap.js: -------------------------------------------------------------------------------- 1 | import { wrapAdapters } from '../adapters/wrap.js' 2 | 3 | import { COMPRESS_ADAPTERS } from './adapters/main.js' 4 | 5 | const members = ['name', 'title', 'decompress', 'compress'] 6 | 7 | export const compressAdapters = wrapAdapters({ 8 | adapters: COMPRESS_ADAPTERS, 9 | members, 10 | reason: 'COMPRESS', 11 | }) 12 | -------------------------------------------------------------------------------- /src/databases/adapters/memory/query/delete.js: -------------------------------------------------------------------------------- 1 | // Delete models 2 | export const deleteMany = ({ collection, deletedIds }) => { 3 | Object.entries(collection) 4 | .filter(([, model]) => deletedIds.includes(model.id)) 5 | // eslint-disable-next-line fp/no-mutating-methods 6 | .map(([index], count) => collection.splice(index - count, 1)[0]) 7 | } 8 | -------------------------------------------------------------------------------- /src/log/adapters/console/main.js: -------------------------------------------------------------------------------- 1 | import { report } from './report.js' 2 | 3 | export const logConsole = { 4 | name: 'console', 5 | title: 'Console log adapter', 6 | description: 7 | 'Log adapter printing on the console, meant as a development helper', 8 | report, 9 | // Do not report `perf` events as it would be too verbose 10 | } 11 | -------------------------------------------------------------------------------- /src/protocols/info.js: -------------------------------------------------------------------------------- 1 | import { getMember, getNames } from '../adapters/get.js' 2 | 3 | import { PROTOCOL_ADAPTERS } from './adapters/main.js' 4 | 5 | export const PROTOCOLS = getNames(PROTOCOL_ADAPTERS) 6 | export const PROTOCOL_OPTS = getMember(PROTOCOL_ADAPTERS, 'opts', {}) 7 | export const PROTOCOL_DEFAULTS = getMember(PROTOCOL_ADAPTERS, 'defaults', {}) 8 | -------------------------------------------------------------------------------- /examples/pet_skills.yml: -------------------------------------------------------------------------------- 1 | name: [skills] 2 | description: Skills of a pet 3 | attributes: 4 | age: 5 | type: integer 6 | description: How old is the pet 7 | default: 1 8 | intelligence: 9 | type: integer 10 | description: How smart is the pet 11 | validate: 12 | required: true 13 | minimum: 14 | $data: 1/age 15 | -------------------------------------------------------------------------------- /src/middleware/sequencer/metadata.js: -------------------------------------------------------------------------------- 1 | import { deepMerge } from '../../utils/functional/merge.js' 2 | 3 | // Deep merge all results' metadata 4 | export const mergeMetadata = ({ results, metadata }) => { 5 | const metadataA = results.map((result) => result.metadata) 6 | const metadataB = deepMerge(metadata, ...metadataA) 7 | return { metadata: metadataB } 8 | } 9 | -------------------------------------------------------------------------------- /src/config/reducers/shortcuts/readonly.js: -------------------------------------------------------------------------------- 1 | import { getShortcut } from '../../helpers.js' 2 | 3 | // Gets a map of collections' readonly attributes, 4 | // e.g. { my_coll: { attribute: 'readonly_value', ... }, ... } 5 | export const readonlyMap = ({ config }) => 6 | getShortcut({ config, filter: 'readonly', mapper }) 7 | 8 | const mapper = ({ readonly }) => readonly 9 | -------------------------------------------------------------------------------- /src/errors/reasons/timeout.js: -------------------------------------------------------------------------------- 1 | // Extra: 2 | // - limit NUM 3 | export const TIMEOUT = { 4 | status: 'CLIENT_ERROR', 5 | title: 'The request took too much time to process', 6 | getMessage: ({ limit }) => 7 | `The request took more than ${ 8 | limit / MILLISECS_TO_SECS 9 | } seconds to process`, 10 | } 11 | 12 | const MILLISECS_TO_SECS = 1e3 13 | -------------------------------------------------------------------------------- /src/middleware/rpc/actions.js: -------------------------------------------------------------------------------- 1 | // Fire actions, unless the response is already known 2 | export const fireActions = ({ response, mInput }, nextLayer) => { 3 | // When the rpc parser already returned the response, 4 | // e.g. with GraphQL introspection queries 5 | if (response) { 6 | return 7 | } 8 | 9 | return nextLayer(mInput, 'action') 10 | } 11 | -------------------------------------------------------------------------------- /src/rpc/adapters/graphql/load/types/object/final_fields/args/params.js: -------------------------------------------------------------------------------- 1 | import { GraphQLJSON } from 'graphql-type-json' 2 | 3 | // `params` argument 4 | export const getParamsArgument = () => PARAMS_ARGS 5 | 6 | const PARAMS_ARGS = { 7 | params: { 8 | type: GraphQLJSON, 9 | description: 'Custom parameters passed to database logic', 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /src/errors/reasons/plugin.js: -------------------------------------------------------------------------------- 1 | const getMessage = ({ plugin }) => { 2 | if (plugin === undefined) { 3 | return 4 | } 5 | 6 | return `In the plugin '${plugin}'` 7 | } 8 | 9 | // Extra: 10 | // - plugin `{string}` 11 | export const PLUGIN = { 12 | status: 'SERVER_ERROR', 13 | title: 'Internal error related to a specific plugin', 14 | getMessage, 15 | } 16 | -------------------------------------------------------------------------------- /src/log/adapters/http/main.js: -------------------------------------------------------------------------------- 1 | import { getOpts } from './get_opts.js' 2 | import { opts } from './opts.js' 3 | import { report } from './report.js' 4 | 5 | export const logHttp = { 6 | name: 'http', 7 | title: 'HTTP log handler', 8 | description: 'Log handler using a HTTP request', 9 | report, 10 | reportPerf: report, 11 | opts, 12 | getOpts, 13 | } 14 | -------------------------------------------------------------------------------- /src/config/reducers/collname.js: -------------------------------------------------------------------------------- 1 | import { mapColls } from '../helpers.js' 2 | 3 | // Default `collection.name` to parent key 4 | const mapColl = ({ collname, coll: { name = [collname] } }) => { 5 | const nameA = Array.isArray(name) ? name : [name] 6 | 7 | return { name: nameA } 8 | } 9 | 10 | export const normalizeClientCollname = mapColls.bind(undefined, mapColl) 11 | -------------------------------------------------------------------------------- /src/compress/info.js: -------------------------------------------------------------------------------- 1 | import compressible from 'compressible' 2 | 3 | import { getNames } from '../adapters/get.js' 4 | 5 | import { COMPRESS_ADAPTERS } from './adapters/main.js' 6 | 7 | export const ALGOS = getNames(COMPRESS_ADAPTERS) 8 | 9 | // Do not try to compress binary content types 10 | export const shouldCompress = ({ contentType }) => compressible(contentType) 11 | -------------------------------------------------------------------------------- /src/middleware/request_response/pagination/encoding/convert_undefined.js: -------------------------------------------------------------------------------- 1 | // Make sure undefined and null compare the same 2 | export const convertUndefined = (token) => { 3 | const parts = token.parts.map(convertToNull) 4 | return { ...token, parts } 5 | } 6 | 7 | // eslint-disable-next-line unicorn/no-null 8 | const convertToNull = (value) => (value === undefined ? null : value) 9 | -------------------------------------------------------------------------------- /src/config/reducers/shortcuts/aliases.js: -------------------------------------------------------------------------------- 1 | import { getShortcut } from '../../helpers.js' 2 | 3 | // Gets a map of collections' attributes' aliases 4 | // e.g. { collname: { attrName: ['alias', ...], ... }, ... } 5 | export const aliasesMap = ({ config }) => 6 | getShortcut({ config, filter: 'alias', mapper }) 7 | 8 | const mapper = ({ alias }) => (Array.isArray(alias) ? alias : [alias]) 9 | -------------------------------------------------------------------------------- /src/rpc/adapters/graphql/parse/definition/merge_select.js: -------------------------------------------------------------------------------- 1 | export const mergeSelectRename = ({ selectRename, name }) => { 2 | const values = selectRename 3 | .map(({ [name]: value }) => value) 4 | .filter((value) => value !== undefined) 5 | 6 | if (values.length === 0) { 7 | return 8 | } 9 | 10 | const valuesA = values.join(',') 11 | return valuesA 12 | } 13 | -------------------------------------------------------------------------------- /docs/client/arguments/dryrun.md: -------------------------------------------------------------------------------- 1 | # Dry runs 2 | 3 | Any [command](../request/crud.md) (except `find`) can perform a dry run by using 4 | the `dryrun` [argument](../rpc/README.md#rpc). 5 | 6 | No modification will be applied to the database, but the response (including 7 | error responses) will be the same as if it did. 8 | 9 | ```HTTP 10 | DELETE /rest/users/1?dryrun=true 11 | ``` 12 | -------------------------------------------------------------------------------- /src/errors/reasons/not_found.js: -------------------------------------------------------------------------------- 1 | import { getModels } from './message.js' 2 | 3 | // Extra: 4 | // - collection `{string}` 5 | // - ids `{string[]}`: models `id`s 6 | export const NOT_FOUND = { 7 | status: 'CLIENT_ERROR', 8 | title: 'Some database models could not be found, e.g. the ids were invalid', 9 | getMessage: (extra) => `${getModels(extra)} could not be found`, 10 | } 11 | -------------------------------------------------------------------------------- /src/protocols/adapters/http/content_negotiation/content_type.js: -------------------------------------------------------------------------------- 1 | import { parseContentType } from '../../../../formats/content_type.js' 2 | 3 | // Parse HTTP header `Content-Type` 4 | export const getContentType = ({ 5 | specific: { 6 | req: { headers }, 7 | }, 8 | }) => { 9 | const contentType = headers['content-type'] 10 | return parseContentType({ contentType }) 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/functional/type.js: -------------------------------------------------------------------------------- 1 | // Is any kind of object, including array, RegExp, Date, Error, etc. 2 | export const isObjectType = (val) => typeof val === 'object' && val !== null 3 | 4 | // Is a plain object, including `Object.create(null)` 5 | export const isObject = (val) => 6 | val !== undefined && 7 | val !== null && 8 | (val.constructor === Object || val.constructor === undefined) 9 | -------------------------------------------------------------------------------- /src/charsets/transform.js: -------------------------------------------------------------------------------- 1 | import iconvLite from 'iconv-lite' 2 | 3 | import { addGenPbHandler } from '../errors/handler.js' 4 | 5 | // Charset decoding 6 | const eDecodeCharset = (charset, content) => iconvLite.decode(content, charset) 7 | 8 | export const decodeCharset = addGenPbHandler(eDecodeCharset, { 9 | reason: 'CHARSET', 10 | extra: (charset) => ({ adapter: charset }), 11 | }) 12 | -------------------------------------------------------------------------------- /src/middleware/protocol/requestid.js: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'node:crypto' 2 | 3 | // Assigns unique ID (UUIDv4) to each request 4 | // Available in mInput, events, system parameters and metadata 5 | export const setRequestid = ({ metadata }) => { 6 | const requestid = randomUUID() 7 | 8 | const metadataA = { ...metadata, requestid } 9 | return { requestid, metadata: metadataA } 10 | } 11 | -------------------------------------------------------------------------------- /src/config/reducers/protocols.js: -------------------------------------------------------------------------------- 1 | import { PROTOCOL_OPTS } from '../../protocols/info.js' 2 | 3 | import { validateAdaptersOpts } from './adapter_opts.js' 4 | 5 | // Validates `protocols.PROTOCOL.*` 6 | export const validateProtocols = ({ config: { protocols } }) => { 7 | validateAdaptersOpts({ 8 | opts: protocols, 9 | adaptersOpts: PROTOCOL_OPTS, 10 | key: 'protocols', 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /src/config/reducers/shortcuts/user_defaults.js: -------------------------------------------------------------------------------- 1 | import { getShortcut } from '../../helpers.js' 2 | 3 | // Retrieves map of collections's attributes for which a default value 4 | // is defined 5 | // E.g. { User: { name: 'default_name', ... }, ... } 6 | export const userDefaultsMap = ({ config }) => 7 | getShortcut({ config, filter: 'default', mapper }) 8 | 9 | const mapper = (attr) => attr.default 10 | -------------------------------------------------------------------------------- /src/formats/info.js: -------------------------------------------------------------------------------- 1 | import { FORMAT_ADAPTERS } from './adapters/main.js' 2 | import { getExtension } from './extensions.js' 3 | 4 | // All possible extensions, for documentation 5 | const getExtensions = () => 6 | FORMAT_ADAPTERS.map((formatAdapter) => getExtension(formatAdapter)).filter( 7 | (extension) => extension !== undefined, 8 | ) 9 | 10 | export const EXTENSIONS = getExtensions() 11 | -------------------------------------------------------------------------------- /src/protocols/request/path.js: -------------------------------------------------------------------------------- 1 | import { validateString } from './validate.js' 2 | 3 | export const parsePath = ({ 4 | protocolAdapter, 5 | protocolAdapter: { getPath }, 6 | specific, 7 | }) => { 8 | if (getPath === undefined) { 9 | return 10 | } 11 | 12 | const path = getPath({ specific }) 13 | 14 | validateString(path, 'path', protocolAdapter) 15 | 16 | return { path } 17 | } 18 | -------------------------------------------------------------------------------- /src/rpc/adapters/graphql/load/types/object/final_fields/args/silent.js: -------------------------------------------------------------------------------- 1 | import { GraphQLBoolean } from 'graphql' 2 | 3 | // `silent` argument 4 | export const getSilentArgument = () => SILENT_ARGS 5 | 6 | const SILENT_ARGS = { 7 | silent: { 8 | type: GraphQLBoolean, 9 | description: `Do not output any result. 10 | The action is still performed.`, 11 | defaultValue: false, 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /src/rpc/adapters/graphql/main.js: -------------------------------------------------------------------------------- 1 | import { load } from './load/main.js' 2 | import { parse } from './parse/main.js' 3 | import { transformError, transformSuccess } from './response.js' 4 | 5 | export const graphql = { 6 | name: 'graphql', 7 | title: 'GraphQL', 8 | routes: ['/graphql'], 9 | methods: ['GET', 'POST'], 10 | parse, 11 | transformSuccess, 12 | transformError, 13 | load, 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/functional/intersection.js: -------------------------------------------------------------------------------- 1 | import { includes } from './includes.js' 2 | import { uniq } from './uniq.js' 3 | 4 | // Like Lodash intersection() 5 | export const intersection = (arrA, arrB, ...arrays) => { 6 | const arrC = arrA.filter((val) => includes(arrB, val)) 7 | 8 | if (arrays.length === 0) { 9 | return uniq(arrC) 10 | } 11 | 12 | return intersection(arrC, ...arrays) 13 | } 14 | -------------------------------------------------------------------------------- /docs/server/plugins/timestamp.md: -------------------------------------------------------------------------------- 1 | # Timestamps 2 | 3 | The [system plugin](README.md) `timestamp` automatically adds for each 4 | collection the attributes: 5 | 6 | - `created_time` `{datetime}` - set on model's creation 7 | - `updated_time` `{datetime}` - set on model's modification 8 | 9 | It is enabled by default. 10 | 11 | ```yml 12 | plugins: 13 | - plugin: timestamp 14 | enabled: false 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/server/protocols/http.md: -------------------------------------------------------------------------------- 1 | # HTTP server options 2 | 3 | HTTP is one of the available [protocols](README.md). 4 | 5 | The HTTP server has the following [options](README.md#options): 6 | 7 | - `hostname` `{string}` (defaults to `localhost`) 8 | - `port` `{integer}` (defaults to `80`). Can be `0` for "any available port". 9 | 10 | ```yml 11 | protocols: 12 | http: 13 | hostname: localhost 14 | port: 80 15 | ``` 16 | -------------------------------------------------------------------------------- /src/errors/reasons/config_validation.js: -------------------------------------------------------------------------------- 1 | const getMessage = ({ path }) => 2 | path === undefined ? undefined : `In configuration property '${path}'` 3 | 4 | // Extra: 5 | // - path 'VARR' 6 | // - value VAL 7 | // - jsonSchema OBJ 8 | // - suggestions VAL_ARR 9 | export const CONFIG_VALIDATION = { 10 | status: 'SERVER_ERROR', 11 | title: 'Wrong configuration caught during server startup', 12 | getMessage, 13 | } 14 | -------------------------------------------------------------------------------- /src/middleware/time/timestamp.js: -------------------------------------------------------------------------------- 1 | import { startPerf } from '../../perf/measure.js' 2 | 3 | // Start the main performance counter, and add request timestamp 4 | export const addTimestamp = () => { 5 | // Used by other middleware 6 | const timestamp = new Date().toISOString() 7 | 8 | // Calculate how long the whole request takes 9 | const reqPerf = startPerf('request') 10 | 11 | return { timestamp, reqPerf } 12 | } 13 | -------------------------------------------------------------------------------- /src/protocols/request/headers.js: -------------------------------------------------------------------------------- 1 | import { validateObject } from './validate.js' 2 | 3 | export const parseHeaders = ({ 4 | protocolAdapter, 5 | protocolAdapter: { getHeaders }, 6 | specific, 7 | }) => { 8 | if (getHeaders === undefined) { 9 | return 10 | } 11 | 12 | const headers = getHeaders({ specific }) 13 | 14 | validateObject(headers, 'headers', protocolAdapter) 15 | 16 | return { headers } 17 | } 18 | -------------------------------------------------------------------------------- /src/databases/adapters/memory/features.js: -------------------------------------------------------------------------------- 1 | export const features = [ 2 | 'filter:_eq', 3 | 'filter:_neq', 4 | 'filter:_lt', 5 | 'filter:_gt', 6 | 'filter:_lte', 7 | 'filter:_gte', 8 | 'filter:_in', 9 | 'filter:_nin', 10 | 'filter:_like', 11 | 'filter:_nlike', 12 | 'filter:_and', 13 | 'filter:_or', 14 | 'filter:_some', 15 | 'filter:_all', 16 | 'filter:sibling', 17 | 'order', 18 | 'offset', 19 | ] 20 | -------------------------------------------------------------------------------- /src/databases/adapters/mongodb/features.js: -------------------------------------------------------------------------------- 1 | export const features = [ 2 | 'filter:_eq', 3 | 'filter:_neq', 4 | 'filter:_lt', 5 | 'filter:_gt', 6 | 'filter:_lte', 7 | 'filter:_gte', 8 | 'filter:_in', 9 | 'filter:_nin', 10 | 'filter:_like', 11 | 'filter:_nlike', 12 | 'filter:_or', 13 | 'filter:_and', 14 | 'filter:_some', 15 | 'filter:_all', 16 | 'filter:sibling', 17 | 'order', 18 | 'offset', 19 | ] 20 | -------------------------------------------------------------------------------- /src/databases/adapters/memory/connect.js: -------------------------------------------------------------------------------- 1 | // Starts connection 2 | export const connect = ({ config, options: { data } }) => { 3 | validateEnv({ config }) 4 | 5 | return data 6 | } 7 | 8 | const validateEnv = ({ config: { env } }) => { 9 | if (env === 'dev') { 10 | return 11 | } 12 | 13 | throw new Error( 14 | "Memory database must not be used in production, i.e. 'config.env' must be equal to 'dev'", 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/protocols/wrap.js: -------------------------------------------------------------------------------- 1 | import { wrapAdapters } from '../adapters/wrap.js' 2 | 3 | import { PROTOCOL_ADAPTERS } from './adapters/main.js' 4 | import { start } from './start.js' 5 | 6 | const members = ['name', 'title'] 7 | 8 | const methods = { 9 | startServer: start, 10 | } 11 | 12 | export const protocolAdapters = wrapAdapters({ 13 | adapters: PROTOCOL_ADAPTERS, 14 | members, 15 | methods, 16 | reason: 'PROTOCOL', 17 | }) 18 | -------------------------------------------------------------------------------- /src/errors/reasons/conflict.js: -------------------------------------------------------------------------------- 1 | import { getModels } from './message.js' 2 | 3 | // Extra: 4 | // - collection `{string}` 5 | // - ids `{string[]}`: models `id`s 6 | export const CONFLICT = { 7 | status: 'CLIENT_ERROR', 8 | title: 'Another client updated the same model, resulting in a conflict', 9 | getMessage: (extra) => 10 | `${getModels(extra)} already ${ 11 | extra.ids.length === 1 ? 'exist' : 'exists' 12 | }`, 13 | } 14 | -------------------------------------------------------------------------------- /src/middleware/action/data_arg/data_path.js: -------------------------------------------------------------------------------- 1 | import { isModelsType } from './validate.js' 2 | 3 | // Retrieve the path to each nested object inside `args.data` 4 | export const getDataPath = ({ data, commandpath }) => { 5 | if (!isModelsType(data)) { 6 | return [] 7 | } 8 | 9 | if (!Array.isArray(data)) { 10 | return [commandpath] 11 | } 12 | 13 | return Object.keys(data).map((index) => [...commandpath, Number(index)]) 14 | } 15 | -------------------------------------------------------------------------------- /src/config/reducers/log_validation.js: -------------------------------------------------------------------------------- 1 | import { LOG_OPTS } from '../../log/info.js' 2 | 3 | import { validateAdaptersOpts } from './adapter_opts.js' 4 | 5 | // Validates `log.LOG.*` 6 | export const validateLogs = ({ config: { log } }) => { 7 | const optsA = log.map(({ provider, opts = {} }) => ({ [provider]: opts })) 8 | const optsB = Object.assign({}, ...optsA) 9 | validateAdaptersOpts({ opts: optsB, adaptersOpts: LOG_OPTS, key: 'log' }) 10 | } 11 | -------------------------------------------------------------------------------- /src/databases/adapters/mongodb/query/find/order.js: -------------------------------------------------------------------------------- 1 | // Apply `args.order` 2 | export const sortResponse = ({ cursor, order }) => { 3 | if (order === undefined) { 4 | return cursor 5 | } 6 | 7 | const orderA = order.map(({ attrName, dir }) => ({ 8 | [attrName]: dir === 'asc' ? 1 : -1, 9 | })) 10 | const orderB = Object.assign({}, ...orderA) 11 | // eslint-disable-next-line fp/no-mutating-methods 12 | return cursor.sort(orderB) 13 | } 14 | -------------------------------------------------------------------------------- /src/middleware/database/rename_ids/order.js: -------------------------------------------------------------------------------- 1 | // Modify `args.order` 2 | export const renameOrder = ({ value, newIdName, oldIdName }) => 3 | value.map((part) => renameOrderPart({ part, newIdName, oldIdName })) 4 | 5 | const renameOrderPart = ({ 6 | part, 7 | part: { attrName }, 8 | newIdName, 9 | oldIdName, 10 | }) => { 11 | if (attrName !== oldIdName) { 12 | return part 13 | } 14 | 15 | return { ...part, attrName: newIdName } 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/errors.js: -------------------------------------------------------------------------------- 1 | // We want to enforce using throwError() when throwing exception 2 | // Therefore, we have ESLint rule no-throw enabled. 3 | // However, utils cannot use normal throwError() without circular 4 | // dependencies 5 | // ESLint rule fp/no-throw helps enforcing this utility is used 6 | export const throwError = (message) => { 7 | if (message instanceof Error) { 8 | throw message 9 | } 10 | 11 | throw new Error(message) 12 | } 13 | -------------------------------------------------------------------------------- /src/middleware/rpc/parse.js: -------------------------------------------------------------------------------- 1 | // Use rpc-specific logic to parse the request into an rpc-agnostic `rpcDef` 2 | export const parseRpc = ({ 3 | rpcAdapter: { parse }, 4 | config, 5 | origin, 6 | queryvars, 7 | headers, 8 | method, 9 | path, 10 | pathvars, 11 | payload, 12 | }) => 13 | parse({ 14 | config, 15 | origin, 16 | queryvars, 17 | headers, 18 | method, 19 | path, 20 | pathvars, 21 | payload, 22 | }) 23 | -------------------------------------------------------------------------------- /src/snapshots/build/src/main.test.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `src/main.test.js` 2 | 3 | The actual snapshot is saved in `main.test.js.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## Smoke test 8 | 9 | > Snapshot 1 10 | 11 | { 12 | message: `HTTP - Listening on␊ 13 | In-Memory - Connection initialized␊ 14 | SUCCESS - HTTP GET REST /rest/pets/2 find_pets␊ 15 | Server is ready`, 16 | stderr: '', 17 | } 18 | -------------------------------------------------------------------------------- /src/config/reducers/required_id.js: -------------------------------------------------------------------------------- 1 | import { mapAttrs } from '../helpers.js' 2 | 3 | // Make sure `id` attributes are required 4 | const mapAttr = ({ 5 | attr: { 6 | validate, 7 | validate: { required }, 8 | }, 9 | attrName, 10 | }) => { 11 | if (attrName !== 'id' || required) { 12 | return 13 | } 14 | 15 | return { validate: { ...validate, required: true } } 16 | } 17 | 18 | export const addRequiredId = mapAttrs.bind(undefined, mapAttr) 19 | -------------------------------------------------------------------------------- /src/filter/parse/optimize.js: -------------------------------------------------------------------------------- 1 | import { mapNodes } from '../crawl.js' 2 | import { getOperator } from '../operators/main.js' 3 | 4 | // Try to simplify AST 5 | export const optimizeFilter = ({ filter }) => mapNodes(filter, optimizeNode) 6 | 7 | const optimizeNode = (node) => { 8 | const { optimize } = getOperator({ node }) 9 | 10 | if (optimize === undefined) { 11 | return node 12 | } 13 | 14 | const nodeA = optimize(node) 15 | return nodeA 16 | } 17 | -------------------------------------------------------------------------------- /src/patch/error.js: -------------------------------------------------------------------------------- 1 | import { OPERATORS } from './operators/main.js' 2 | 3 | // Properties of errors during `patch` 4 | // We want to differentiate between errors due to engine bug or wrong config 5 | export const getPatchErrorProps = ({ type, extra }) => { 6 | if (OPERATORS[type] !== undefined) { 7 | return { reason: 'ENGINE' } 8 | } 9 | 10 | const path = `config.operators.${type}` 11 | return { reason: 'CONFIG_RUNTIME', extra: { ...extra, path } } 12 | } 13 | -------------------------------------------------------------------------------- /docs/dev/ROADMAP.md: -------------------------------------------------------------------------------- 1 | Some of the major planned features, by priority order: 2 | 3 | - Proper error reporting 4 | - Aggregation queries 5 | - Tasks 6 | - Testing 7 | - HTTPS and HTTP/2 8 | - Static assets 9 | - Compatibility layer 10 | - Orphans handling 11 | - Polymorphism 12 | - Real-time and concurrency conflicts 13 | - Views 14 | - Authentication 15 | - Logging/monitoring dashboard 16 | - Security 17 | - Caching 18 | - Performance optimization 19 | - Client generation 20 | -------------------------------------------------------------------------------- /src/log/adapters/console/report.js: -------------------------------------------------------------------------------- 1 | import { colorize } from './colorize.js' 2 | import { getConsoleMessage } from './message.js' 3 | 4 | // Prints event messages to console. 5 | export const report = ({ log, log: { level } }) => { 6 | const consoleMessage = getConsoleMessage({ log }) 7 | 8 | const consoleMessageA = colorize({ log, consoleMessage }) 9 | 10 | // eslint-disable-next-line no-console, no-restricted-globals 11 | console[level](consoleMessageA) 12 | } 13 | -------------------------------------------------------------------------------- /src/errors/reasons/authorization.js: -------------------------------------------------------------------------------- 1 | import { getModels } from './message.js' 2 | 3 | // Extra: 4 | // - collection `{string}` 5 | // - ids `{string[]}`: models `id`s 6 | export const AUTHORIZATION = { 7 | status: 'CLIENT_ERROR', 8 | title: 'The request is not authorized, i.e. not allowed to be performed', 9 | getMessage: ({ 10 | top: { 11 | command: { participle }, 12 | }, 13 | ...extra 14 | }) => `${getModels(extra)} cannot be ${participle}`, 15 | } 16 | -------------------------------------------------------------------------------- /src/errors/reasons/method.js: -------------------------------------------------------------------------------- 1 | import { getWordsList } from '../../utils/string.js' 2 | 3 | const getMessage = ({ value, suggestions }) => { 4 | const protocols = getWordsList(suggestions, { op: 'or' }) 5 | return `Protocol ${value} is invalid: it should be ${protocols}` 6 | } 7 | 8 | // Extra: 9 | // - value STR 10 | // - suggestions STR_ARR 11 | export const METHOD = { 12 | status: 'CLIENT_ERROR', 13 | title: 'The protocol method is invalid', 14 | getMessage, 15 | } 16 | -------------------------------------------------------------------------------- /src/rpc/adapters/graphql/parse/fragments.js: -------------------------------------------------------------------------------- 1 | import { validateDuplicates } from './duplicates.js' 2 | 3 | // Retrieve GraphQL fragments 4 | export const getFragments = ({ queryDocument: { definitions } }) => { 5 | const fragments = definitions.filter( 6 | ({ kind }) => kind === 'FragmentDefinition', 7 | ) 8 | 9 | // GraphQL spec 5.4.1.1 'Fragment Name Uniqueness' 10 | validateDuplicates({ nodes: fragments, type: 'fragments' }) 11 | 12 | return fragments 13 | } 14 | -------------------------------------------------------------------------------- /src/middleware/request_response/empty_models.js: -------------------------------------------------------------------------------- 1 | // Remove models that are null|undefined 2 | // Those can happen with some database. E.g. MongoDB sometimes release read 3 | // locks in the middle of a query, which can result in the same model appearing 4 | // twice in the response. 5 | export const removeEmptyModels = ({ response: { data, ...rest } }) => { 6 | const dataA = data.filter((datum) => datum !== undefined && datum !== null) 7 | return { response: { data: dataA, ...rest } } 8 | } 9 | -------------------------------------------------------------------------------- /docs/client/arguments/sorting.md: -------------------------------------------------------------------------------- 1 | # Sorting 2 | 3 | One can sort the output of `find` commands, using `order`. 4 | 5 | ```HTTP 6 | GET /rest/users/?order=name 7 | ``` 8 | 9 | `order` defaults to `id`. 10 | 11 | To sort in the opposite order, append `-` to the attribute. 12 | 13 | ```HTTP 14 | GET /rest/users/?order=name- 15 | ``` 16 | 17 | To sort according to several attributes, separate them with a comma. 18 | 19 | ```HTTP 20 | GET /rest/users/?order=first_name,last_name 21 | ``` 22 | -------------------------------------------------------------------------------- /src/middleware/action/assemble.js: -------------------------------------------------------------------------------- 1 | import { set } from '../../utils/functional/get_set.js' 2 | 3 | // Merge all `results` into a single nested response, using `result.path` 4 | export const assembleResults = ({ results, top: { command } }) => { 5 | const response = command.multiple ? [] : {} 6 | const responseA = results.reduce(assembleResult, response) 7 | return { response: responseA } 8 | } 9 | 10 | const assembleResult = (response, { model, path }) => set(response, path, model) 11 | -------------------------------------------------------------------------------- /src/rpc/adapters/graphql/load/types/object/filter.js: -------------------------------------------------------------------------------- 1 | export const filterField = (def, opts) => { 2 | const isFiltered = filters.some((filter) => filter(def, opts)) 3 | // eslint-disable-next-line unicorn/no-null 4 | return isFiltered ? null : def 5 | } 6 | 7 | // `patch` does not allow `data.id` 8 | const patchIdData = ({ command }, { inputObjectType, defName }) => 9 | inputObjectType === 'data' && command === 'patch' && defName === 'id' 10 | 11 | const filters = [patchIdData] 12 | -------------------------------------------------------------------------------- /docs/server/data_model/compatibility.md: -------------------------------------------------------------------------------- 1 | # Aliases 2 | 3 | Attributes can specify alternative names that clients can use, e.g. for backward 4 | compatibility, using the `attribute.alias` 5 | [configuration property](../configuration/configuration.md#properties), which 6 | can be a string or an array of strings. 7 | 8 | ```yml 9 | collections: 10 | example_collection: 11 | attributes: 12 | example_attribute: 13 | alias: [old_attribute_name, older_attribute_name] 14 | ``` 15 | -------------------------------------------------------------------------------- /src/middleware/final/perf_event.js: -------------------------------------------------------------------------------- 1 | import { logPerfEvent } from '../../log/perf.js' 2 | 3 | // Event performance events related to the current request, 4 | // e.g. how long each middleware lasted. 5 | export const perfEvent = ( 6 | { config, mInput, respPerf }, 7 | nextLayer, 8 | { measures }, 9 | ) => { 10 | const measuresA = [...measures, respPerf] 11 | return logPerfEvent({ 12 | mInput, 13 | phase: 'request', 14 | measures: measuresA, 15 | config, 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /src/middleware/final/send_response/params.js: -------------------------------------------------------------------------------- 1 | import { getSumParams } from '../../../utils/sums.js' 2 | 3 | // Add `response`-related parameters 4 | export const getResponseParams = ({ type, content }) => { 5 | // `responsedatasize` and `responsedatacount` parameters 6 | const sumParams = getSumParams({ attrName: 'responsedata', value: content }) 7 | 8 | return { 9 | response: content, 10 | responsetype: type, 11 | responsedata: content, 12 | ...sumParams, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/middleware/final/status.js: -------------------------------------------------------------------------------- 1 | import { getProps } from '../../errors/props.js' 2 | 3 | // Retrieve response's status 4 | // TODO: why is this called twice??? 5 | export const getStatus = ({ error }) => { 6 | const { status = 'SERVER_ERROR' } = getProps(error) 7 | const level = STATUS_LEVEL_MAP[status] 8 | return { status, level } 9 | } 10 | 11 | const STATUS_LEVEL_MAP = { 12 | INTERNALS: 'debug', 13 | SUCCESS: 'log', 14 | CLIENT_ERROR: 'warn', 15 | SERVER_ERROR: 'error', 16 | } 17 | -------------------------------------------------------------------------------- /src/compress/adapters/gzip.js: -------------------------------------------------------------------------------- 1 | import { promisify } from 'node:util' 2 | import { gunzip as zlibGunzip, gzip as zlibGzip } from 'node:zlib' 3 | 4 | const pGzip = promisify(zlibGzip) 5 | const pGunzip = promisify(zlibGunzip) 6 | 7 | // Compress to Gzip 8 | const compress = (content) => pGzip(content) 9 | 10 | // Decompress from Gzip 11 | const decompress = (content) => pGunzip(content) 12 | 13 | export const gzip = { 14 | name: 'gzip', 15 | title: 'Gzip', 16 | compress, 17 | decompress, 18 | } 19 | -------------------------------------------------------------------------------- /src/json_refs/find.js: -------------------------------------------------------------------------------- 1 | import { getValues } from '../utils/functional/values.js' 2 | 3 | // Recursively find all the JSON references 4 | export const findRefs = ({ content }) => 5 | getValues(content).filter(isRef).map(removeLastPath) 6 | 7 | const isRef = ({ value, keys }) => 8 | typeof value === 'string' && keys.at(-1) === '$ref' 9 | 10 | // Remove `$ref` from keys 11 | const removeLastPath = ({ value, keys }) => { 12 | const keysA = keys.slice(0, -1) 13 | return { value, keys: keysA } 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/functional/promise.js: -------------------------------------------------------------------------------- 1 | // Similar to `await retVal` and `Promise.resolve(retVal).then()` 2 | // As opposed to them, this does not create a new promise callback if the 3 | // return value is synchronous, i.e. it avoids unnecessary new microtasks 4 | export const promiseThen = (retVal, func) => { 5 | if (!retVal || typeof retVal.then !== 'function') { 6 | return func(retVal) 7 | } 8 | 9 | // eslint-disable-next-line promise/prefer-await-to-then 10 | return retVal.then(func) 11 | } 12 | -------------------------------------------------------------------------------- /src/rpc/adapters/graphql/load/main.js: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema } from 'graphql' 2 | 3 | import { getTopDefs } from './top_defs.js' 4 | import { getTopTypes } from './type.js' 5 | 6 | // Add GraphQL schema, so it can be used by introspection, and by graphqlPrint 7 | export const load = ({ config: { collections } }) => { 8 | const topDefs = getTopDefs({ collections }) 9 | const topTypes = getTopTypes({ topDefs }) 10 | const graphqlSchema = new GraphQLSchema(topTypes) 11 | return { graphqlSchema } 12 | } 13 | -------------------------------------------------------------------------------- /src/json_refs/ref_path.js: -------------------------------------------------------------------------------- 1 | import { isObjectType } from '../utils/functional/type.js' 2 | 3 | const REF_SYM = Symbol('ref') 4 | 5 | // Remember original JSON reference absolute path so it can be used later, 6 | // for example to serialize back 7 | export const setRef = ({ content, path }) => { 8 | if (!isObjectType(content)) { 9 | return content 10 | } 11 | 12 | const contentA = { ...content, [REF_SYM]: path } 13 | return contentA 14 | } 15 | 16 | export const getRef = (content) => content[REF_SYM] 17 | -------------------------------------------------------------------------------- /src/log/adapters/http/get_opts.js: -------------------------------------------------------------------------------- 1 | // Parse `opts.url`, also ensuring it is a valid URL 2 | export const getOpts = ({ opts: { url } }) => { 3 | if (url === undefined) { 4 | return 5 | } 6 | 7 | const { 8 | hostname, 9 | port, 10 | auth, 11 | pathname = '', 12 | search = '', 13 | hash = '', 14 | } = new URL(url) 15 | const portA = port ? Number(port) : undefined 16 | const path = `${pathname}${search}${hash}` 17 | 18 | return { hostname, port: portA, auth, path } 19 | } 20 | -------------------------------------------------------------------------------- /src/middleware/request_response/duplicate_read.js: -------------------------------------------------------------------------------- 1 | import { uniq } from '../../utils/functional/uniq.js' 2 | 3 | // Remove duplicate read models 4 | // Those can happen with some database. E.g. MongoDB sometimes release read 5 | // locks in the middle of a query, which can result in the same model appearing 6 | // twice in the response. 7 | export const duplicateReads = ({ response: { data, ...rest } }) => { 8 | const dataA = uniq(data, ({ id }) => id) 9 | return { response: { data: dataA, ...rest } } 10 | } 11 | -------------------------------------------------------------------------------- /src/rpc/wrap.js: -------------------------------------------------------------------------------- 1 | import { wrapAdapters } from '../adapters/wrap.js' 2 | 3 | import { RPC_ADAPTERS } from './adapters/main.js' 4 | import { checkMethod } from './method_check.js' 5 | import { transformResponse } from './transform.js' 6 | 7 | const members = ['name', 'title', 'load', 'parse'] 8 | 9 | const methods = { 10 | checkMethod, 11 | transformResponse, 12 | } 13 | 14 | export const rpcAdapters = wrapAdapters({ 15 | adapters: RPC_ADAPTERS, 16 | members, 17 | methods, 18 | reason: 'RPC', 19 | }) 20 | -------------------------------------------------------------------------------- /src/functions/params/keys.js: -------------------------------------------------------------------------------- 1 | import { 2 | LATER_SYSTEM_PARAMS, 3 | POSITIONAL_PARAMS, 4 | SYSTEM_PARAMS, 5 | TEMP_SYSTEM_PARAMS, 6 | } from './system.js' 7 | 8 | // Retrieve parameters names 9 | export const getParamsKeys = ({ config: { params = {} } }) => { 10 | const namedKeys = [ 11 | ...Object.keys(SYSTEM_PARAMS), 12 | ...LATER_SYSTEM_PARAMS, 13 | ...TEMP_SYSTEM_PARAMS, 14 | ...Object.keys(params), 15 | ] 16 | const posKeys = POSITIONAL_PARAMS 17 | return { namedKeys, posKeys } 18 | } 19 | -------------------------------------------------------------------------------- /src/middleware/action/rename_args.js: -------------------------------------------------------------------------------- 1 | import underscoreString from 'underscore.string' 2 | 3 | import { mapKeys } from '../../utils/functional/map.js' 4 | 5 | // Change arguments cases to camelCase 6 | export const renameArgs = ({ actions }) => { 7 | const actionsA = actions.map(renameActionArgs) 8 | return { actions: actionsA } 9 | } 10 | 11 | const renameActionArgs = ({ args, ...action }) => { 12 | const argsA = mapKeys(args, (arg, name) => underscoreString.camelize(name)) 13 | return { ...action, args: argsA } 14 | } 15 | -------------------------------------------------------------------------------- /src/log/perf.js: -------------------------------------------------------------------------------- 1 | import { groupMeasures } from '../perf/group.js' 2 | import { stringifyMeasures } from '../perf/stringify.js' 3 | 4 | import { logEvent } from './main.js' 5 | 6 | // Emit 'perf' event 7 | export const logPerfEvent = ({ phase, measures, ...rest }) => { 8 | const measuresGroups = groupMeasures({ measures }) 9 | const measuresmessage = stringifyMeasures({ phase, measuresGroups }) 10 | const params = { measures: measuresGroups, measuresmessage } 11 | return logEvent({ ...rest, event: 'perf', phase, params }) 12 | } 13 | -------------------------------------------------------------------------------- /docs/server/usage/error.md: -------------------------------------------------------------------------------- 1 | # Exceptions 2 | 3 | Exceptions contain the same properties as the `error` property of an 4 | [error response](../../client/request/error.md#error-responses). They also 5 | include a `details` property with a stack trace. 6 | 7 | When an [instruction](README.md) fails, its promise is rejected with an 8 | exception object. 9 | 10 | When a client-side or server-side error occurs, an exception will be 11 | [logged](../quality/logging.md) using the 12 | [`error` parameter](../quality/logging.md#functions-parameters). 13 | -------------------------------------------------------------------------------- /src/compress/adapters/deflate.js: -------------------------------------------------------------------------------- 1 | import { promisify } from 'node:util' 2 | import { deflate as zlibDeflate, inflate as zlibInflate } from 'node:zlib' 3 | 4 | const pDeflate = promisify(zlibDeflate) 5 | const pInflate = promisify(zlibInflate) 6 | 7 | // Compress to Deflate 8 | const compress = (content) => pDeflate(content) 9 | 10 | // Decompress from Deflate 11 | const decompress = (content) => pInflate(content) 12 | 13 | export const deflate = { 14 | name: 'deflate', 15 | title: 'Deflate', 16 | compress, 17 | decompress, 18 | } 19 | -------------------------------------------------------------------------------- /src/formats/adapters/json.js: -------------------------------------------------------------------------------- 1 | // Parses a JSON file 2 | const parse = ({ content }) => JSON.parse(content) 3 | 4 | // Serializes a JSON file 5 | const serialize = ({ content }) => JSON.stringify(content, undefined, 2) 6 | 7 | export const json = { 8 | name: 'json', 9 | title: 'JSON', 10 | extensions: ['json'], 11 | mimes: ['application/json'], 12 | mimeExtensions: ['+json'], 13 | // eslint-disable-next-line unicorn/text-encoding-identifier-case 14 | charsets: ['utf-8'], 15 | jsonCompat: [], 16 | parse, 17 | serialize, 18 | } 19 | -------------------------------------------------------------------------------- /src/rpc/adapters/graphiql/parse/main.js: -------------------------------------------------------------------------------- 1 | import { renderGraphiql } from './render.js' 2 | 3 | // Render GraphiQL HTML file, i.e. GraphQL debugger 4 | export const parse = async ({ queryvars, payload = {}, origin }) => { 5 | const endpointURL = `${origin}/graphql` 6 | const { query, variables, operationName } = { ...queryvars, ...payload } 7 | 8 | const content = await renderGraphiql({ 9 | endpointURL, 10 | query, 11 | variables, 12 | operationName, 13 | }) 14 | 15 | return { response: { type: 'html', content } } 16 | } 17 | -------------------------------------------------------------------------------- /src/run/exit/protocol_close.js: -------------------------------------------------------------------------------- 1 | import { wrapCloseFunc } from './wrapper.js' 2 | 3 | // Attempts to close server 4 | // No new connections will be accepted, but we will wait for ongoing ones to end 5 | export const closeProtocols = ({ protocolAdapters, config, measures }) => 6 | Object.values(protocolAdapters).map((adapter) => 7 | eCloseProtocol({ type: 'protocols', adapter, config, measures }), 8 | ) 9 | 10 | const closeProtocol = ({ adapter: { stopServer } }) => stopServer() 11 | 12 | const eCloseProtocol = wrapCloseFunc(closeProtocol) 13 | -------------------------------------------------------------------------------- /src/databases/adapters/mongodb/query/delete.js: -------------------------------------------------------------------------------- 1 | // Delete models 2 | export const deleteFunc = ({ collection, deletedIds }) => { 3 | const func = deletedIds.length === 1 ? deleteOne : deleteMany 4 | return func({ collection, deletedIds }) 5 | } 6 | 7 | const deleteOne = ({ collection, deletedIds }) => { 8 | const [_id] = deletedIds 9 | return collection.deleteOne({ _id }) 10 | } 11 | 12 | const deleteMany = ({ collection, deletedIds }) => { 13 | const filter = { _id: { $in: deletedIds } } 14 | return collection.deleteMany(filter) 15 | } 16 | -------------------------------------------------------------------------------- /src/middleware/sequencer/write/current_data.js: -------------------------------------------------------------------------------- 1 | // Retrieve `currentData`, so it is passed to command middleware 2 | export const getCurrentData = ({ actions, ids }) => { 3 | const currentData = actions.flatMap((action) => action.currentData) 4 | // Keep the same order as `newData` or `args.filter.id` 5 | const currentDataA = ids.map((id) => findCurrentData({ id, currentData })) 6 | return currentDataA 7 | } 8 | 9 | const findCurrentData = ({ id, currentData }) => 10 | currentData.find((currentDatum) => currentDatum && currentDatum.id === id) 11 | -------------------------------------------------------------------------------- /src/protocols/request/queryvars.js: -------------------------------------------------------------------------------- 1 | import { getFormat } from '../../formats/get.js' 2 | 3 | import { validateString } from './validate.js' 4 | 5 | export const parseQueryvars = ({ 6 | protocolAdapter, 7 | protocolAdapter: { getQueryString }, 8 | specific, 9 | }) => { 10 | const queryString = getQueryString({ specific }) 11 | 12 | validateString(queryString, 'queryString', protocolAdapter) 13 | 14 | const queryvars = urlencoded.parseContent(queryString) 15 | return { queryvars } 16 | } 17 | 18 | const urlencoded = getFormat('urlencoded') 19 | -------------------------------------------------------------------------------- /src/config/reducers/rpc.js: -------------------------------------------------------------------------------- 1 | import { getRpc } from '../../rpc/get.js' 2 | import { RPCS } from '../../rpc/info.js' 3 | 4 | // Fire each `rpcAdapter.load({ config })` function 5 | export const loadRpc = ({ config }) => { 6 | const output = RPCS.map((rpc) => loadSingleRpc({ rpc, config })) 7 | const outputA = Object.assign({}, ...output) 8 | return outputA 9 | } 10 | 11 | const loadSingleRpc = ({ rpc, config }) => { 12 | const { load } = getRpc(rpc) 13 | 14 | if (load === undefined) { 15 | return 16 | } 17 | 18 | return load({ config }) 19 | } 20 | -------------------------------------------------------------------------------- /src/formats/adapters/main.js: -------------------------------------------------------------------------------- 1 | import { hjson } from './hjson.js' 2 | import { ini } from './ini.js' 3 | import { javascript } from './javascript.js' 4 | import { json } from './json.js' 5 | import { json5 } from './json5.js' 6 | import { raw } from './raw.js' 7 | import { urlencoded } from './urlencoded.js' 8 | import { yaml } from './yaml.js' 9 | 10 | // Order matters, as first ones will have priority 11 | export const FORMAT_ADAPTERS = [ 12 | json, 13 | yaml, 14 | urlencoded, 15 | javascript, 16 | hjson, 17 | json5, 18 | ini, 19 | raw, 20 | ] 21 | -------------------------------------------------------------------------------- /src/middleware/request_response/features.js: -------------------------------------------------------------------------------- 1 | import { addGenErrorHandler } from '../../errors/handler.js' 2 | 3 | // Validate database supports command features 4 | const eValidateRuntimeFeatures = ({ 5 | args, 6 | collname, 7 | clientCollname, 8 | dbAdapters, 9 | }) => { 10 | const dbAdapter = dbAdapters[collname] 11 | return dbAdapter.validateRuntimeFeatures({ args, clientCollname }) 12 | } 13 | 14 | export const validateRuntimeFeatures = addGenErrorHandler( 15 | eValidateRuntimeFeatures, 16 | { 17 | reason: 'VALIDATION', 18 | }, 19 | ) 20 | -------------------------------------------------------------------------------- /src/middleware/request_response/normalize_empty.js: -------------------------------------------------------------------------------- 1 | import { excludeKeys } from 'filter-obj' 2 | 3 | // Normalize empty values (undefined, null) by removing their key 4 | export const normalizeEmpty = ({ args, args: { newData } }) => { 5 | if (newData === undefined) { 6 | return 7 | } 8 | 9 | const newDataA = newData.map(removeEmpty) 10 | return { args: { ...args, newData: newDataA } } 11 | } 12 | 13 | const removeEmpty = (newData) => excludeKeys(newData, hasNoValue) 14 | 15 | const hasNoValue = (key, value) => value === undefined || value === null 16 | -------------------------------------------------------------------------------- /src/utils/functional/values.js: -------------------------------------------------------------------------------- 1 | import { isObject } from './type.js' 2 | 3 | // Returns all leaves values (i.e. not objects|arrays) as a list of 4 | // `{ value, key [...] }` pairs 5 | export const getValues = (value, keys = []) => { 6 | if (Array.isArray(value)) { 7 | return value.flatMap((valueA, key) => getValues(valueA, [...keys, key])) 8 | } 9 | 10 | if (isObject(value)) { 11 | return Object.entries(value).flatMap(([key, valueA]) => 12 | getValues(valueA, [...keys, key]), 13 | ) 14 | } 15 | 16 | return [{ value, keys }] 17 | } 18 | -------------------------------------------------------------------------------- /docs/server/protocols/README.md: -------------------------------------------------------------------------------- 1 | # Protocols 2 | 3 | Several network protocols can be handled at the same time, each spawning a 4 | single server. By default, all protocols are spawned. 5 | 6 | # Options 7 | 8 | Each protocol has its own set of options, which are specified with the 9 | `protocols` 10 | [configuration property](../configuration/configuration.md#properties). 11 | 12 | ```yml 13 | protocols: 14 | http: 15 | port: 80 16 | ``` 17 | 18 | will launch a HTTP server on port `80`. 19 | 20 | # Available protocols 21 | 22 | - [`http`](http.md): HTTP/1.1 23 | -------------------------------------------------------------------------------- /src/rpc/adapters/graphql/load/types/object/final_fields/args/filter.js: -------------------------------------------------------------------------------- 1 | import { getArgTypeDescription } from '../../../../description.js' 2 | 3 | // `filter` argument 4 | export const getFilterArgument = (def, { filterObjectType }) => { 5 | const hasFilter = FILTER_COMMAND_TYPES.has(def.command) 6 | 7 | if (!hasFilter) { 8 | return {} 9 | } 10 | 11 | const description = getArgTypeDescription(def, 'argFilter') 12 | 13 | return { filter: { type: filterObjectType, description } } 14 | } 15 | 16 | const FILTER_COMMAND_TYPES = new Set(['find', 'delete', 'patch']) 17 | -------------------------------------------------------------------------------- /src/middleware/final/duration.js: -------------------------------------------------------------------------------- 1 | import { stopPerf } from '../../perf/measure.js' 2 | 3 | // Request response time, from request handling start to response sending 4 | // Note that other functions might happen after response sending, e.g. events 5 | export const setDuration = ({ reqPerf, metadata }) => { 6 | const respPerf = stopPerf(reqPerf) 7 | 8 | const duration = Math.round(respPerf.duration / MICROSECS_TO_SECS) 9 | 10 | const metadataA = { ...metadata, duration } 11 | return { respPerf, duration, metadata: metadataA } 12 | } 13 | 14 | const MICROSECS_TO_SECS = 1e6 15 | -------------------------------------------------------------------------------- /src/config/reducers/shortcuts/colls_names.js: -------------------------------------------------------------------------------- 1 | // Returns a map from clientCollname to collname 2 | // Example: { my_name: 'my_coll', ... } 3 | export const collsNames = ({ config: { collections } }) => { 4 | const map = Object.entries(collections).map(([collname, { name }]) => 5 | getNames({ collname, name }), 6 | ) 7 | const mapA = Object.assign({}, ...map) 8 | return mapA 9 | } 10 | 11 | const getNames = ({ collname, name }) => { 12 | const names = name.map((nameA) => ({ [nameA]: collname })) 13 | const namesA = Object.assign({}, ...names) 14 | return namesA 15 | } 16 | -------------------------------------------------------------------------------- /src/middleware/final/send_response/error.js: -------------------------------------------------------------------------------- 1 | import omit from 'omit.js' 2 | 3 | import { getStandardError } from '../../../errors/standard.js' 4 | 5 | // Use protocol-specific way to send back the response to the client 6 | export const getErrorResponse = ({ error, mInput, response }) => { 7 | if (!error) { 8 | return response 9 | } 10 | 11 | const content = getStandardError({ error, mInput }) 12 | 13 | // Do not show stack trace in error responses 14 | const contentA = omit.default(content, ['details']) 15 | 16 | return { type: 'error', content: contentA } 17 | } 18 | -------------------------------------------------------------------------------- /docs/dev/development.md: -------------------------------------------------------------------------------- 1 | # Installing dependencies 2 | 3 | Run `npm install`. 4 | 5 | # Build tasks 6 | 7 | You can run the following tasks on the command line: 8 | 9 | - `gulp`: build and run an example HTTP server at `http://localhost:5001` in 10 | watch mode. The server's configuration is available at `/examples`. 11 | - `gulp build`: build the application 12 | - `gulp test`: test and lint the application 13 | - `gulp buildwatch`, `gulp testwatch`: like `gulp build` and `gulp test` but in 14 | watch mode 15 | 16 | For more information on the available tasks, run `gulp --tasks`. 17 | -------------------------------------------------------------------------------- /docs/server/quality/README.md: -------------------------------------------------------------------------------- 1 | # Software quality 2 | 3 | Software quality is ensured in several ways: 4 | 5 | - every server [event](logging.md#events), including requests and errors, can be 6 | [logged](logging.md). Every log contains full 7 | [information](logging.md#functions-parameters) about the context. 8 | - performance can be [monitored](logging.md#performance-monitoring) 9 | - the API is [auto-documented](documentation.md) (as a GraphQL schema only for 10 | the moment) 11 | - client requests are constrained by [limits](limits.md) to guarantee a proper 12 | level of service 13 | -------------------------------------------------------------------------------- /src/databases/adapters/memory/query/upsert.js: -------------------------------------------------------------------------------- 1 | // Upsert models 2 | export const upsert = ({ collection, newData }) => { 3 | newData.forEach((datum) => { 4 | upsertOne({ collection, datum }) 5 | }) 6 | } 7 | 8 | const upsertOne = ({ collection, datum }) => { 9 | const index = collection.findIndex(({ id }) => id === datum.id) 10 | 11 | if (index === -1) { 12 | // eslint-disable-next-line fp/no-mutating-methods 13 | collection.push(datum) 14 | return 15 | } 16 | 17 | // eslint-disable-next-line fp/no-mutating-methods 18 | collection.splice(index, 1, datum) 19 | } 20 | -------------------------------------------------------------------------------- /src/serverinfo/versions.js: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path' 2 | import { version as nodeVersion } from 'node:process' 3 | import { fileURLToPath } from 'node:url' 4 | 5 | import { readPackageUp } from 'read-package-up' 6 | 7 | // Caches it. 8 | const { 9 | packageJson: { version: autoserverVersion }, 10 | } = await readPackageUp({ cwd: dirname(fileURLToPath(import.meta.url)) }) 11 | 12 | // Retrieve environment-specific versions 13 | export const getVersionsInfo = () => { 14 | const autoserver = `v${autoserverVersion}` 15 | 16 | return { node: nodeVersion, autoserver } 17 | } 18 | -------------------------------------------------------------------------------- /src/databases/adapters/mongodb/main.js: -------------------------------------------------------------------------------- 1 | /* jscpd:ignore-start */ 2 | import { connect } from './connect.js' 3 | import { defaults } from './defaults.js' 4 | import { disconnect } from './disconnect.js' 5 | import { features } from './features.js' 6 | import { opts } from './opts.js' 7 | import { query } from './query/main.js' 8 | /* jscpd:ignore-end */ 9 | 10 | export const mongodb = { 11 | name: 'mongodb', 12 | title: 'MongoDB', 13 | description: 'MongoDB database', 14 | features, 15 | connect, 16 | disconnect, 17 | query, 18 | defaults, 19 | opts, 20 | idName: '_id', 21 | } 22 | -------------------------------------------------------------------------------- /src/compress/adapters/brotli.js: -------------------------------------------------------------------------------- 1 | import { promisify } from 'node:util' 2 | import { brotliCompress, brotliDecompress } from 'node:zlib' 3 | 4 | // Compress to Brotli 5 | const compress = (content) => { 6 | const pBrotliCompress = promisify(brotliCompress) 7 | return pBrotliCompress(content) 8 | } 9 | 10 | // Decompress from Brotli 11 | const decompress = (content) => { 12 | const pBrotliDecompress = promisify(brotliDecompress) 13 | return pBrotliDecompress(content) 14 | } 15 | 16 | export const brotli = { 17 | name: 'br', 18 | title: 'Brotli', 19 | compress, 20 | decompress, 21 | } 22 | -------------------------------------------------------------------------------- /src/middleware/database/rename_ids/data.js: -------------------------------------------------------------------------------- 1 | import omit from 'omit.js' 2 | 3 | // Modify `args.newData`, or database output 4 | export const renameData = ({ value, newIdName, oldIdName }) => 5 | value.map((datum) => renameDatum({ datum, newIdName, oldIdName })) 6 | 7 | const renameDatum = ({ datum, newIdName, oldIdName }) => { 8 | const hasId = Object.keys(datum).includes(oldIdName) 9 | 10 | if (!hasId) { 11 | return datum 12 | } 13 | 14 | const { [oldIdName]: attr } = datum 15 | const datumA = omit.default(datum, [oldIdName]) 16 | return { ...datumA, [newIdName]: attr } 17 | } 18 | -------------------------------------------------------------------------------- /src/rpc/adapters/graphql/parse/duplicates.js: -------------------------------------------------------------------------------- 1 | import { throwError } from '../../../../errors/main.js' 2 | import { findDuplicate } from '../../../../utils/functional/uniq.js' 3 | 4 | // GraphQL spec includes many requirements of checking for duplicates 5 | export const validateDuplicates = ({ nodes, type }) => { 6 | const names = nodes.map(({ name }) => name && name.value) 7 | const nameA = findDuplicate(names) 8 | 9 | if (nameA === undefined) { 10 | return 11 | } 12 | 13 | const message = `Two ${type} are named '${nameA}'` 14 | throwError(message, { reason: 'VALIDATION' }) 15 | } 16 | -------------------------------------------------------------------------------- /src/formats/adapters/json5.js: -------------------------------------------------------------------------------- 1 | import json5Lib from 'json5' 2 | 3 | // Parses a JSON5 file 4 | const parse = ({ content }) => json5Lib.parse(content) 5 | 6 | // Serializes a JSON5 file 7 | const serialize = ({ content }) => json5Lib.stringify(content, undefined, 2) 8 | 9 | export const json5 = { 10 | name: 'json5', 11 | title: 'JSON5', 12 | extensions: ['json5'], 13 | mimes: ['application/json5'], 14 | mimeExtensions: ['+json5'], 15 | // eslint-disable-next-line unicorn/text-encoding-identifier-case 16 | charsets: ['utf-8'], 17 | jsonCompat: ['superset'], 18 | parse, 19 | serialize, 20 | } 21 | -------------------------------------------------------------------------------- /src/middleware/request_response/aliases/order.js: -------------------------------------------------------------------------------- 1 | import { isObject } from '../../../utils/functional/type.js' 2 | 3 | // Copy first defined alias to main attribute, providing it is not defined. 4 | export const applyOrderAliases = ({ order, attrName, aliases }) => { 5 | if (!Array.isArray(order)) { 6 | return order 7 | } 8 | 9 | return order.map((orderPart) => { 10 | if (!isObject(orderPart)) { 11 | return orderPart 12 | } 13 | 14 | if (!aliases.includes(orderPart.attrName)) { 15 | return orderPart 16 | } 17 | 18 | return { ...orderPart, attrName } 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /src/patch/parse.js: -------------------------------------------------------------------------------- 1 | import { isObject } from '../utils/functional/type.js' 2 | 3 | // Check if this is a patch operation, e.g. `{ _add: 10 }` 4 | export const isPatchOp = (patchOp) => 5 | isObject(patchOp) && Object.keys(patchOp).some(isPatchOpName) 6 | 7 | // Patch operations are prefixed with _ to differentiate from nested attributes 8 | export const isPatchOpName = (key) => key.startsWith('_') 9 | 10 | export const parsePatchOp = (patchOp) => { 11 | if (!isPatchOp(patchOp)) { 12 | return {} 13 | } 14 | 15 | const [[type, opVal]] = Object.entries(patchOp) 16 | return { type, opVal } 17 | } 18 | -------------------------------------------------------------------------------- /src/filter/operators/eq_neq.js: -------------------------------------------------------------------------------- 1 | import { isDeepStrictEqual } from 'node:util' 2 | 3 | import { parseAsIs, validateSameType } from './common.js' 4 | 5 | // `{ attribute: { _eq: value } }` or `{ attribute: value }` 6 | const evalEq = ({ attr, value }) => isDeepStrictEqual(attr, value) 7 | 8 | // `{ attribute: { _neq: value } }` 9 | const evalNeq = ({ attr, value }) => !isDeepStrictEqual(attr, value) 10 | 11 | export const eq = { 12 | parse: parseAsIs, 13 | validate: validateSameType, 14 | eval: evalEq, 15 | } 16 | export const neq = { 17 | parse: parseAsIs, 18 | validate: validateSameType, 19 | eval: evalNeq, 20 | } 21 | -------------------------------------------------------------------------------- /src/log/adapters/http/opts.js: -------------------------------------------------------------------------------- 1 | export const opts = { 2 | type: 'object', 3 | additionalProperties: false, 4 | required: ['url'], 5 | properties: { 6 | url: { 7 | type: 'string', 8 | }, 9 | method: { 10 | type: 'string', 11 | enum: ['GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'DELETE'], 12 | }, 13 | hostname: { 14 | type: 'string', 15 | }, 16 | port: { 17 | type: 'integer', 18 | minimum: 0, 19 | maximum: 65_535, 20 | }, 21 | auth: { 22 | type: 'string', 23 | }, 24 | path: { 25 | type: 'string', 26 | }, 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/functional/once.js: -------------------------------------------------------------------------------- 1 | import { throwError } from '../errors.js' 2 | 3 | // Enforces that a function is only called once 4 | export const once = (func, { error = false } = {}) => { 5 | // eslint-disable-next-line fp/no-let 6 | let called = false 7 | 8 | return (...args) => { 9 | if (called) { 10 | return alreadyCalled({ error }) 11 | } 12 | 13 | // eslint-disable-next-line fp/no-mutation 14 | called = true 15 | return func(...args) 16 | } 17 | } 18 | 19 | const alreadyCalled = ({ error }) => { 20 | if (error) { 21 | throwError('This function can only be called once') 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /docs/server/data_model/default.md: -------------------------------------------------------------------------------- 1 | # Default values 2 | 3 | Default values for attributes can be specified with the `attribute.default` 4 | [configuration property](../configuration/configuration.md#properties). 5 | 6 | ```yml 7 | collections: 8 | example_collection: 9 | attributes: 10 | example_attribute: 11 | default: 200 12 | ``` 13 | 14 | They will be used for `create` and `upsert` commands. 15 | 16 | [Functions](../configuration/functions.md) can be used. 17 | 18 | ```yml 19 | collections: 20 | example_collection: 21 | attributes: 22 | example_attribute: 23 | default: (timestamp) 24 | ``` 25 | -------------------------------------------------------------------------------- /src/middleware/action/sort.js: -------------------------------------------------------------------------------- 1 | import { compareArrays, sortArray } from '../../utils/functional/sort.js' 2 | 3 | const sorter = (obj, key, pathKey) => { 4 | const val = sortArray(obj[key], sortTwo.bind(undefined, pathKey)) 5 | return { [key]: val } 6 | } 7 | 8 | const sortTwo = (pathKey, objA, objB) => 9 | compareArrays(objA[pathKey], objB[pathKey]) 10 | 11 | // Sort `actions` so that top-level ones are fired first 12 | export const sortActions = (obj) => sorter(obj, 'actions', 'commandpath') 13 | 14 | // Sort `results` so that top-level ones are processed first 15 | export const sortResults = (obj) => sorter(obj, 'results', 'path') 16 | -------------------------------------------------------------------------------- /src/run/error.js: -------------------------------------------------------------------------------- 1 | import { logEvent } from '../log/main.js' 2 | 3 | // Handle exceptions thrown at server startup 4 | export const handleStartupError = async ( 5 | error, 6 | { exitFunc, protocolAdapters, dbAdapters, config }, 7 | ) => { 8 | // Make sure servers are properly closed if an exception is thrown at end 9 | // of startup, e.g. during start event handler 10 | if (exitFunc !== undefined) { 11 | await exitFunc({ protocolAdapters, dbAdapters }) 12 | } 13 | 14 | await logEvent({ 15 | event: 'failure', 16 | phase: 'startup', 17 | params: { error }, 18 | config, 19 | }) 20 | 21 | throw error 22 | } 23 | -------------------------------------------------------------------------------- /src/middleware/final/call_event.js: -------------------------------------------------------------------------------- 1 | import { logEvent } from '../../log/main.js' 2 | import { nanoSecsToMilliSecs } from '../../perf/measure.js' 3 | 4 | // Main "call" event middleware. 5 | // Each request creates exactly one "call" event, whether successful or not 6 | export const callEvent = ({ 7 | config, 8 | level, 9 | mInput, 10 | error, 11 | respPerf: { duration } = {}, 12 | }) => { 13 | const durationA = nanoSecsToMilliSecs(duration) 14 | 15 | return logEvent({ 16 | mInput, 17 | event: 'call', 18 | phase: 'request', 19 | level, 20 | params: { error, duration: durationA }, 21 | config, 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /src/rpc/adapters/rest/parse.js: -------------------------------------------------------------------------------- 1 | import { getArgs } from './args.js' 2 | 3 | // Use JSON-RPC-specific logic to parse the request into an 4 | // rpc-agnostic `rpcDef` 5 | export const parse = ({ 6 | payload, 7 | method, 8 | queryvars, 9 | pathvars: { clientCollname, id }, 10 | }) => { 11 | const commandName = `${METHODS_MAP[method]}_${clientCollname}` 12 | const args = getArgs({ method, payload, queryvars, id }) 13 | return { rpcDef: { commandName, args } } 14 | } 15 | 16 | const METHODS_MAP = { 17 | GET: 'find', 18 | HEAD: 'find', 19 | POST: 'create', 20 | PUT: 'upsert', 21 | PATCH: 'patch', 22 | DELETE: 'delete', 23 | } 24 | -------------------------------------------------------------------------------- /src/databases/adapters/memory/main.js: -------------------------------------------------------------------------------- 1 | /* jscpd:ignore-start */ 2 | import { check } from './check.js' 3 | import { connect } from './connect.js' 4 | import { defaults } from './defaults.js' 5 | import { disconnect } from './disconnect.js' 6 | import { features } from './features.js' 7 | import { opts } from './opts.js' 8 | import { query } from './query/main.js' 9 | /* jscpd:ignore-end */ 10 | 11 | export const memory = { 12 | name: 'memory', 13 | title: 'In-Memory', 14 | description: 'In-memory database. For development purpose only.', 15 | features, 16 | connect, 17 | check, 18 | disconnect, 19 | query, 20 | defaults, 21 | opts, 22 | } 23 | -------------------------------------------------------------------------------- /src/errors/instruction.js: -------------------------------------------------------------------------------- 1 | import { addErrorHandler } from './handler.js' 2 | import { getStandardError } from './standard.js' 3 | 4 | // Every instruction should throw standard errors 5 | export const wrapInstruction = (instructionName, instruction) => 6 | addErrorHandler( 7 | instruction, 8 | instructionHandler.bind(undefined, instructionName), 9 | ) 10 | 11 | const instructionHandler = (instructionName, error) => { 12 | const { 13 | description = `Could not perform instruction '${instructionName}'.`, 14 | ...errorA 15 | } = getStandardError({ error }) 16 | 17 | const errorB = { ...errorA, description } 18 | throw errorB 19 | } 20 | -------------------------------------------------------------------------------- /src/databases/adapters/memory/query/find/main.js: -------------------------------------------------------------------------------- 1 | import { evalFilter } from '../../../../../filter/eval.js' 2 | 3 | import { limitResponse } from './limit.js' 4 | import { offsetResponse } from './offset.js' 5 | import { sortResponse } from './order.js' 6 | 7 | // Retrieve models 8 | export const find = ({ collection, filter, order, offset, limit }) => { 9 | const data = collection.filter((model) => 10 | evalFilter({ attrs: model, filter }), 11 | ) 12 | 13 | const dataA = sortResponse({ data, order }) 14 | const dataB = offsetResponse({ data: dataA, offset }) 15 | const dataC = limitResponse({ data: dataB, limit }) 16 | 17 | return dataC 18 | } 19 | -------------------------------------------------------------------------------- /src/protocols/adapters/http/ip.js: -------------------------------------------------------------------------------- 1 | import { getClientIp } from 'request-ip' 2 | 3 | // Retrieves request IP. 4 | // Tries, in order: 5 | // - X-Client-IP [C] 6 | // - X-Forwarded-For [C] 7 | // - CF-Connecting-IP [C] 8 | // - True-Client-Ip [C] 9 | // - X-Real-IP [C] 10 | // - X-Cluster-Client-IP [C] 11 | // - X-Forwarded [C] 12 | // - Forwarded-For [C] 13 | // - REQ.connection.remoteAddress 14 | // - REQ.connection.socket.remoteAddress 15 | // - REQ.socket.remoteAddress 16 | // - REQ.info.remoteAddress 17 | // If invalid IPv4|IPv6, throws. 18 | // If unknown, returns undefined. 19 | export const getIp = ({ specific: { req } }) => getClientIp(req) || '' 20 | -------------------------------------------------------------------------------- /src/formats/adapters/hjson.js: -------------------------------------------------------------------------------- 1 | import { parse as hjsonParse, stringify as hjsonStringify } from 'hjson' 2 | 3 | // Parses a HJSON file 4 | const parse = ({ content }) => hjsonParse(content) 5 | 6 | // Serializes a HJSON file 7 | const serialize = ({ content }) => 8 | hjsonStringify(content, { bracesSameLine: true }) 9 | 10 | export const hjson = { 11 | name: 'hjson', 12 | title: 'Hjson', 13 | extensions: ['hjson'], 14 | mimes: ['application/hjson', 'text/hjson'], 15 | mimeExtensions: ['+hjson'], 16 | // eslint-disable-next-line unicorn/text-encoding-identifier-case 17 | charsets: ['utf-8'], 18 | jsonCompat: [], 19 | parse, 20 | serialize, 21 | } 22 | -------------------------------------------------------------------------------- /src/middleware/request_response/pagination/encoding/minify_names.js: -------------------------------------------------------------------------------- 1 | import { invert } from '../../../../utils/functional/invert.js' 2 | import { mapKeys } from '../../../../utils/functional/map.js' 3 | 4 | // Name shortcuts, e.g. { filter: value } -> { f: value } 5 | export const addNameShortcuts = (token) => 6 | mapKeys(token, (value, attrName) => SHORTCUTS[attrName] || attrName) 7 | 8 | export const removeNameShortcuts = (token) => 9 | mapKeys(token, (value, attrName) => REVERSE_SHORTCUTS[attrName] || attrName) 10 | 11 | const SHORTCUTS = { 12 | filter: 'f', 13 | order: 'o', 14 | parts: 'p', 15 | } 16 | 17 | const REVERSE_SHORTCUTS = invert(SHORTCUTS) 18 | -------------------------------------------------------------------------------- /src/run/perf.js: -------------------------------------------------------------------------------- 1 | import { logPerfEvent } from '../log/perf.js' 2 | import { startPerf, stopPerf } from '../perf/measure.js' 3 | 4 | // Monitor startup time 5 | export const startStartupPerf = () => { 6 | const startupPerf = startPerf('startup') 7 | return { startupPerf } 8 | } 9 | 10 | export const stopStartupPerf = ({ startupPerf, measures }) => { 11 | const startupPerfA = stopPerf(startupPerf) 12 | const measuresA = [startupPerfA, ...measures] 13 | return { measures: measuresA } 14 | } 15 | 16 | // Emit "perf" event with startup performance 17 | export const reportStartupPerf = ({ config, measures }) => 18 | logPerfEvent({ phase: 'startup', config, measures }) 19 | -------------------------------------------------------------------------------- /src/utils/base64.js: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'node:buffer' 2 | 3 | // Encodes and decodes base64 (RFC 4648) 4 | // We use the `base64url` variant, as we need it to be URL-friendly 5 | // This is much faster than libraries like js-base64 6 | export const base64UrlEncode = (str) => { 7 | const strA = Buffer.from(str).toString('base64') 8 | const strB = strA 9 | .replaceAll('+', '-') 10 | .replaceAll('/', '_') 11 | .replaceAll('=', '') 12 | return strB 13 | } 14 | 15 | export const base64UrlDecode = (str) => { 16 | const strA = str.replaceAll('-', '+').replaceAll('_', '/') 17 | const strB = Buffer.from(strA, 'base64').toString() 18 | return strB 19 | } 20 | -------------------------------------------------------------------------------- /src/middleware/request_response/aliases/response.js: -------------------------------------------------------------------------------- 1 | // Apply `alias` in responses 2 | export const applyResponseAliases = ({ data, attrName, aliases }) => 3 | data.map((datum) => applyResponseAlias({ data: datum, attrName, aliases })) 4 | 5 | // Copy main attribute's value to each alias, providing main attribute is 6 | // defined 7 | const applyResponseAlias = ({ data, attrName, aliases }) => { 8 | const shouldSetAliases = Object.keys(data).includes(attrName) 9 | 10 | if (!shouldSetAliases) { 11 | return data 12 | } 13 | 14 | const aliasesObj = aliases.map((alias) => ({ [alias]: data[attrName] })) 15 | 16 | return Object.assign({}, data, ...aliasesObj) 17 | } 18 | -------------------------------------------------------------------------------- /src/middleware/request_response/create_ids.js: -------------------------------------------------------------------------------- 1 | import { throwPb } from '../../errors/props.js' 2 | 3 | // Check if any model already exists, for create actions 4 | export const validateCreateIds = ({ 5 | response: { data }, 6 | command, 7 | top: { 8 | command: { type: topCommand }, 9 | }, 10 | clientCollname, 11 | }) => { 12 | const isCreateCurrentData = topCommand === 'create' && command === 'find' 13 | 14 | if (!isCreateCurrentData) { 15 | return 16 | } 17 | 18 | if (data.length === 0) { 19 | return 20 | } 21 | 22 | const ids = data.map(({ id }) => id) 23 | throwPb({ reason: 'CONFLICT', extra: { collection: clientCollname, ids } }) 24 | } 25 | -------------------------------------------------------------------------------- /src/rpc/adapters/graphql/load/types/object/final_fields/args/dryrun.js: -------------------------------------------------------------------------------- 1 | import { GraphQLBoolean } from 'graphql' 2 | 3 | // `dryrun` argument 4 | export const getDryrunArgument = ({ command }) => { 5 | const hasDryrun = DRYRUN_COMMANDS.has(command) 6 | 7 | if (!hasDryrun) { 8 | return {} 9 | } 10 | 11 | return DRYRUN_ARGS 12 | } 13 | 14 | const DRYRUN_COMMANDS = new Set(['create', 'upsert', 'patch', 'delete']) 15 | 16 | const DRYRUN_ARGS = { 17 | dryrun: { 18 | type: GraphQLBoolean, 19 | description: 20 | 'No modification will be applied to the database, but the response will be the same as if it did.', 21 | defaultValue: false, 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /src/run/exit/message.js: -------------------------------------------------------------------------------- 1 | import { logEvent } from '../../log/main.js' 2 | 3 | export const emitMessageEvent = ({ 4 | step, 5 | type, 6 | adapter: { title }, 7 | config, 8 | }) => { 9 | const message = SUCCESS_MESSAGES[type][step] 10 | const messageA = `${title} - ${message}` 11 | 12 | return logEvent({ 13 | event: 'message', 14 | phase: 'shutdown', 15 | message: messageA, 16 | config, 17 | }) 18 | } 19 | 20 | const SUCCESS_MESSAGES = { 21 | protocols: { 22 | start: 'Starts shutdown', 23 | end: 'Successful shutdown', 24 | }, 25 | databases: { 26 | start: 'Starts disconnection', 27 | end: 'Successful disconnection', 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /docs/client/arguments/selecting.md: -------------------------------------------------------------------------------- 1 | # Selection 2 | 3 | The `select` [argument](../rpc/README.md#rpc) can be used to filter which 4 | attributes are present in the response. 5 | 6 | It is a comma-separated list of attribute names. 7 | [Nested attributes](../request/relations.md#populating-nested-collections) can 8 | be specified using a dot notation. 9 | 10 | ```HTTP 11 | GET /rest/users/1?select=name 12 | ``` 13 | 14 | ```json 15 | { 16 | "data": { 17 | "name": "Anthony" 18 | } 19 | } 20 | ``` 21 | 22 | [GraphQL](../rpc/graphql.md#selection-population-and-renaming) does not need the 23 | `select` [argument](../rpc/README.md#rpc) since it natively uses selection 24 | fields. 25 | -------------------------------------------------------------------------------- /src/config/reducers/nested_coll.js: -------------------------------------------------------------------------------- 1 | import { mapAttrs } from '../helpers.js' 2 | 3 | // Copy `attr.type|description` to nested collections 4 | // from the `coll.id` they refer to 5 | const mapAttr = ({ attr, config: { collections } }) => { 6 | if (attr.target === undefined) { 7 | return 8 | } 9 | 10 | const [, collA] = Object.entries(collections).find( 11 | ([name, coll]) => coll.collname === attr.target || name === attr.target, 12 | ) 13 | 14 | const { type } = collA.attributes.id 15 | const description = attr.description || collA.description 16 | 17 | return { type, description } 18 | } 19 | 20 | export const mergeNestedColl = mapAttrs.bind(undefined, mapAttr) 21 | -------------------------------------------------------------------------------- /src/databases/wrap.js: -------------------------------------------------------------------------------- 1 | import { wrapAdapters } from '../adapters/wrap.js' 2 | 3 | import { DATABASE_ADAPTERS } from './adapters/main.js' 4 | import { connectDatabase } from './connect.js' 5 | import { validateRuntimeFeatures } from './features/runtime.js' 6 | import { validateStartupFeatures } from './features/startup.js' 7 | 8 | const members = ['name', 'title', 'idName', 'features', 'getDefaultId'] 9 | 10 | const methods = { 11 | connect: connectDatabase, 12 | validateStartupFeatures, 13 | validateRuntimeFeatures, 14 | } 15 | 16 | export const databaseAdapters = wrapAdapters({ 17 | adapters: DATABASE_ADAPTERS, 18 | members, 19 | methods, 20 | reason: 'DATABASE', 21 | }) 22 | -------------------------------------------------------------------------------- /src/middleware/action/modelscount.js: -------------------------------------------------------------------------------- 1 | import { uniq } from '../../utils/functional/uniq.js' 2 | 3 | // Add `modelscount` and `uniquecount`, using `results`. 4 | // `modelscount` is the number of models in the response 5 | // `uniquecount` is the same, without the duplicates 6 | export const getModelscount = ({ results }) => { 7 | const modelscount = results.length 8 | const uniquecount = getUniquecount({ results }) 9 | 10 | return { modelscount, uniquecount } 11 | } 12 | 13 | const getUniquecount = ({ results }) => { 14 | const keys = uniq(results, getCollnameId) 15 | return keys.length 16 | } 17 | 18 | const getCollnameId = ({ action: { collname }, model: { id } }) => 19 | `${collname} ${id}` 20 | -------------------------------------------------------------------------------- /src/middleware/action/server_params.js: -------------------------------------------------------------------------------- 1 | import { getServerParams } from '../../functions/params/server.js' 2 | 3 | // Bind server-specific parameters with their parameters 4 | // This middleware needs to be: 5 | // - late enough to pass as many parameters as possible to 6 | // server-specific parameters 7 | // - early enough to be before any config function is fired 8 | // - only passing parameters that are not changed through the request. 9 | // For example `collection` should not be available to server-specific 10 | // parameters. 11 | export const bindServerParams = ({ config, mInput }) => { 12 | const serverParams = getServerParams({ config, mInput }) 13 | return { serverParams } 14 | } 15 | -------------------------------------------------------------------------------- /src/middleware/final/send_response/validate.js: -------------------------------------------------------------------------------- 1 | import { throwPb } from '../../../errors/props.js' 2 | 3 | import { TYPES } from './types.js' 4 | 5 | export const validateResponse = ({ response: { type, content } }) => { 6 | if (!type) { 7 | const message = 'Server sent an response with no content type' 8 | throwPb({ message, reason: 'ENGINE' }) 9 | } 10 | 11 | if (content === undefined) { 12 | const message = 'Server sent an empty response' 13 | throwPb({ message, reason: 'ENGINE' }) 14 | } 15 | 16 | if (TYPES[type] === undefined) { 17 | const message = 'Server tried to respond with an unsupported content type' 18 | throwPb({ message, reason: 'ENGINE' }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/middleware/request_response/aliases/output.js: -------------------------------------------------------------------------------- 1 | import { applyResponseAliases } from './response.js' 2 | 3 | // Apply `alias` in server output 4 | export const applyOutputAliases = ({ response, modelAliases }) => { 5 | const responseB = Object.entries(modelAliases).reduce( 6 | (responseA, [attrName, aliases]) => 7 | applyOutputAlias({ response: responseA, attrName, aliases }), 8 | response, 9 | ) 10 | return { response: responseB } 11 | } 12 | 13 | const applyOutputAlias = ({ 14 | response, 15 | response: { data }, 16 | attrName, 17 | aliases, 18 | }) => { 19 | const dataA = applyResponseAliases({ data, attrName, aliases }) 20 | return { ...response, data: dataA } 21 | } 22 | -------------------------------------------------------------------------------- /src/rpc/adapters/graphql/load/types/object/final_fields/args/id.js: -------------------------------------------------------------------------------- 1 | import { GraphQLString } from 'graphql' 2 | 3 | import { getArgTypeDescription } from '../../../../description.js' 4 | 5 | // `id` argument 6 | export const getIdArgument = (def) => { 7 | const hasId = ID_COMMAND_TYPES.has(def.command) 8 | 9 | if (!hasId) { 10 | return {} 11 | } 12 | 13 | const description = getArgTypeDescription(def, 'argId') 14 | 15 | const args = getIdArgs({ description }) 16 | return args 17 | } 18 | 19 | const ID_COMMAND_TYPES = new Set(['find', 'delete', 'patch']) 20 | 21 | const getIdArgs = ({ description }) => ({ 22 | id: { 23 | type: GraphQLString, 24 | description, 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /src/config/reducers/shortcuts/main.js: -------------------------------------------------------------------------------- 1 | import { mapValues } from '../../../utils/functional/map.js' 2 | 3 | import { aliasesMap } from './aliases.js' 4 | import { collsNames } from './colls_names.js' 5 | import { readonlyMap } from './readonly.js' 6 | import { userDefaultsMap } from './user_defaults.js' 7 | import { valuesMap } from './value.js' 8 | 9 | // Startup transformations just meant for runtime performance optimization 10 | export const normalizeShortcuts = ({ config }) => { 11 | const shortcuts = mapValues(MAPS, (func) => func({ config })) 12 | return { shortcuts } 13 | } 14 | 15 | const MAPS = { 16 | aliasesMap, 17 | collsNames, 18 | readonlyMap, 19 | userDefaultsMap, 20 | valuesMap, 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/functional/uniq.js: -------------------------------------------------------------------------------- 1 | import { includes } from './includes.js' 2 | 3 | // Like Lodash uniq(), but deep equal, and can use mapper functions 4 | export const uniq = (arr, mapper) => { 5 | const mappedArr = mapper === undefined ? arr : arr.map(mapper) 6 | return arr.filter((val, index) => isUnique(mappedArr, index)) 7 | } 8 | 9 | // Returns first duplicate 10 | export const findDuplicate = (arr, mapper) => { 11 | const mappedArr = mapper === undefined ? arr : arr.map(mapper) 12 | return arr.find((val, index) => !isUnique(mappedArr, index)) 13 | } 14 | 15 | const isUnique = (mappedArr, index) => { 16 | const nextVals = mappedArr.slice(index + 1) 17 | return !includes(nextVals, mappedArr[index]) 18 | } 19 | -------------------------------------------------------------------------------- /src/perf/sort.js: -------------------------------------------------------------------------------- 1 | import sortOn from 'sort-on' 2 | 3 | // Sort by category (asc) then by duration (desc) 4 | export const sortMeasures = (measuresGroups) => 5 | sortOn(measuresGroups, [getCategoryIndex, '-average']) 6 | 7 | const getCategoryIndex = ({ category }) => CATEGORIES.indexOf(category) 8 | 9 | // Order matters, as console printing uses it for sorting 10 | const CATEGORIES = [ 11 | 'cli', 12 | 'default', 13 | 'main', 14 | 'config', 15 | 'run_opts', 16 | 'databases', 17 | 'protocols', 18 | 'middleware', 19 | 'time', 20 | 'protocol', 21 | 'protoparse', 22 | 'rpc', 23 | 'action', 24 | 'read', 25 | 'write', 26 | 'request', 27 | 'database', 28 | 'response', 29 | 'final', 30 | ] 31 | -------------------------------------------------------------------------------- /src/rpc/adapters/graphql/load/types/object/final_fields/args/cascade.js: -------------------------------------------------------------------------------- 1 | import { GraphQLString } from 'graphql' 2 | 3 | // `cascade` argument 4 | export const getCascadeArgument = ({ command }) => { 5 | const hasCascade = CASCADE_COMMANDS.has(command) 6 | 7 | if (!hasCascade) { 8 | return {} 9 | } 10 | 11 | return CASCADE_ARGS 12 | } 13 | 14 | const CASCADE_COMMANDS = new Set(['delete']) 15 | 16 | const CASCADE_ARGS = { 17 | cascade: { 18 | type: GraphQLString, 19 | description: `Also delete specified nested collections. 20 | Each attribute can use dot-delimited notation to specify deeply nested collections. 21 | Several attributes can specified, by using a comma-separated list.`, 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /src/config/reducers/load/main.js: -------------------------------------------------------------------------------- 1 | import { dereferenceRefs } from '../../../json_refs/main.js' 2 | import { deepMerge } from '../../../utils/functional/merge.js' 3 | 4 | import { getEnvVars } from './env.js' 5 | import { getConfPath } from './path.js' 6 | 7 | // Load config file 8 | export const loadFile = async ({ configPath, config: configOpts }) => { 9 | const { config: envConfigPath, ...envVars } = getEnvVars() 10 | 11 | const path = await getConfPath({ envConfigPath, configPath }) 12 | 13 | const configFile = await dereferenceRefs({ path }) 14 | 15 | // Priority order: environment variables > Node.js/CLI options > config file 16 | const config = deepMerge(configFile, configOpts, envVars) 17 | 18 | return config 19 | } 20 | -------------------------------------------------------------------------------- /src/middleware/sequencer/read/input.js: -------------------------------------------------------------------------------- 1 | import { validateMaxmodels } from './limits.js' 2 | import { getParentIds, getParentResults } from './parent_results.js' 3 | 4 | // Retrieve the main information we need to perform the commands 5 | export const getInput = ({ 6 | action: { commandpath }, 7 | results, 8 | maxmodels, 9 | top, 10 | }) => { 11 | const parentResults = getParentResults({ commandpath, results }) 12 | const commandName = commandpath.at(-1) 13 | const { nestedParentIds, parentIds, allIds } = getParentIds({ 14 | commandName, 15 | parentResults, 16 | }) 17 | 18 | validateMaxmodels({ results, allIds, maxmodels, top }) 19 | 20 | return { parentResults, commandName, nestedParentIds, parentIds } 21 | } 22 | -------------------------------------------------------------------------------- /src/rpc/adapters/graphqlprint/parse/main.js: -------------------------------------------------------------------------------- 1 | import { printSchema } from 'graphql' 2 | 3 | import { renderTemplate } from '../../../../utils/template.js' 4 | 5 | const TEMPLATE = new URL('print.mustache', import.meta.url) 6 | 7 | // Print GraphQL schema as beautified HTML 8 | export const parse = async ({ config: { graphqlSchema } }) => { 9 | const graphqlPrintedSchema = await printGraphqlSchema({ graphqlSchema }) 10 | 11 | const content = await renderTemplate({ 12 | template: TEMPLATE, 13 | data: { graphqlPrintedSchema, prismVersion: '1.6.0' }, 14 | }) 15 | 16 | return { response: { type: 'html', content } } 17 | } 18 | 19 | const printGraphqlSchema = ({ graphqlSchema }) => 20 | printSchema(graphqlSchema).trim() 21 | -------------------------------------------------------------------------------- /docs/server/plugins/author.md: -------------------------------------------------------------------------------- 1 | # Model authors 2 | 3 | The [system plugin](README.md) `author` automatically adds for each collection 4 | the attributes: 5 | 6 | - `created_by` `{user}` - set on model's creation 7 | - `updated_by` `{user}` - set on model's modification 8 | 9 | It is not enabled by default. 10 | 11 | The following plugin options must be specified: 12 | 13 | - `currentuser` [`{function}`](../configuration/functions.md): retrieves the 14 | current request's user. Cannot return null if the user is anonymous. 15 | - `collection` `{string}`: user's collection name. 16 | 17 | ```yml 18 | plugins: 19 | - plugin: author 20 | opts: 21 | currentuser: 22 | $ref: get_user.js 23 | collection: users 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/server/quality/documentation.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Descriptions can be specified with the `collection.description` and 4 | `attribute.description` 5 | [configuration properties](../configuration/configuration.md#properties). 6 | 7 | Additionally attributes can be documented as deprecated by specifying 8 | `attribute.deprecation_reason`. 9 | 10 | Also, examples can be documented using `attribute.examples` array of strings. 11 | 12 | ```yml 13 | collections: 14 | example_collection: 15 | description: Description of this collection 16 | attributes: 17 | name: 18 | description: Name of a user 19 | deprecation_reason: Please use the attribute new_name instead 20 | examples: [John, Mary] 21 | ``` 22 | -------------------------------------------------------------------------------- /src/formats/wrap.js: -------------------------------------------------------------------------------- 1 | import { wrapAdapters } from '../adapters/wrap.js' 2 | 3 | import { FORMAT_ADAPTERS } from './adapters/main.js' 4 | import { getCharset, hasCharset } from './charset.js' 5 | import { parseContent, serializeContent } from './content.js' 6 | import { getExtension } from './extensions.js' 7 | import { parseFile, serializeFile } from './file.js' 8 | 9 | const members = ['name', 'title', 'unsafe'] 10 | 11 | const methods = { 12 | getCharset, 13 | hasCharset, 14 | parseContent, 15 | serializeContent, 16 | parseFile, 17 | serializeFile, 18 | getExtension, 19 | } 20 | 21 | export const formatAdapters = wrapAdapters({ 22 | adapters: FORMAT_ADAPTERS, 23 | members, 24 | methods, 25 | reason: 'FORMAT', 26 | }) 27 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "autoserver", 3 | "projectOwner": "ehmicky", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "linkToUsage": false, 12 | "contributors": [ 13 | { 14 | "login": "ehmicky", 15 | "name": "ehmicky", 16 | "avatar_url": "https://avatars2.githubusercontent.com/u/8136211?v=4", 17 | "profile": "https://fosstodon.org/@ehmicky", 18 | "contributions": [ 19 | "code", 20 | "design", 21 | "ideas", 22 | "doc" 23 | ] 24 | } 25 | ], 26 | "contributorsPerLine": 7, 27 | "skipCi": true, 28 | "commitConvention": "none" 29 | } 30 | -------------------------------------------------------------------------------- /docs/client/request/README.md: -------------------------------------------------------------------------------- 1 | # Requests 2 | 3 | Client can perform the following [CRUD commands](crud.md): 4 | [`find`](crud.md#find-command), [`create`](crud.md#create-command), 5 | [`upsert`](crud.md#upsert-command), [`patch`](crud.md#patch-command), 6 | [`delete`](crud.md#delete-command). 7 | 8 | [`patch` commands](patch.md) can perform 9 | [advanced mutations](patch.md#advanced-patch-command) such as `_add` or `_push`. 10 | 11 | Nested collections can be deeply 12 | [populated](relations.md#populating-nested-collections), 13 | [modified](relations.md#modifying-nested-collections) and 14 | [deleted](relations.md#deleting-nested-collections). 15 | 16 | If anything went wrong, an [error response](error.md#error-responses) will be 17 | sent. 18 | -------------------------------------------------------------------------------- /src/rpc/adapters/graphqlprint/parse/print.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 |
15 | {{{graphqlPrintedSchema}}}
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/middleware/error/handler.js:
--------------------------------------------------------------------------------
1 | import { logEvent } from '../../log/main.js'
2 |
3 | // Error handler, which sends final response, if server-side errors
4 | export const errorHandler = async ({
5 | error,
6 | protocolAdapter,
7 | config,
8 | mInput,
9 | }) => {
10 | // Make sure a response is sent, even empty, or the socket will hang
11 | await protocolAdapter.send({ content: '', contentLength: 0 })
12 |
13 | // In case an error happened during final layer
14 | const mInputA = { ...mInput, status: 'SERVER_ERROR' }
15 |
16 | // Report any exception thrown
17 | await logEvent({
18 | mInput: mInputA,
19 | event: 'failure',
20 | phase: 'request',
21 | level: 'error',
22 | params: { error },
23 | config,
24 | })
25 | }
26 |
--------------------------------------------------------------------------------
/src/middleware/request_response/authorize/data.js:
--------------------------------------------------------------------------------
1 | import { throwPb } from '../../../errors/props.js'
2 | import { evalFilter } from '../../../filter/eval.js'
3 |
4 | // Check `model.authorize` `model.*` against `args.newData`
5 | export const checkNewData = ({
6 | authorize,
7 | args: { newData },
8 | clientCollname,
9 | top,
10 | }) => {
11 | if (newData === undefined) {
12 | return
13 | }
14 |
15 | const ids = newData
16 | .filter((datum) => !evalFilter({ filter: authorize, attrs: datum }))
17 | .map(({ id }) => id)
18 |
19 | if (ids.length === 0) {
20 | return
21 | }
22 |
23 | throwPb({
24 | reason: 'AUTHORIZATION',
25 | messageInput: { top },
26 | extra: { collection: clientCollname, ids },
27 | })
28 | }
29 |
--------------------------------------------------------------------------------
/docs/client/protocols/README.md:
--------------------------------------------------------------------------------
1 | # Protocols
2 |
3 | Client requests can use several protocols (only HTTP at the moment).
4 |
5 | Each protocol has its own way of specifying the request headers, method, URL and
6 | payload.
7 |
8 | For example, HTTP uses HTTP headers and HTTP method (e.g. `GET`).
9 |
10 | The client request payload and the server response can use any of the following
11 | [formats](formats.md): [JSON](formats.md#json), [YAML](formats.md#yaml),
12 | [x-www-form-urlencoded](formats.md#x-www-form-urlencoded),
13 | [raw](formats.md#raw), [Hjson](formats.md#hjson), [JSON5](formats.md#json5) and
14 | [INI](formats.md#ini). Different [charsets](formats.md#charsets) can also be
15 | used.
16 |
17 | # Available protocols
18 |
19 | - [`http`](http.md): HTTP/1.1
20 |
--------------------------------------------------------------------------------
/src/patch/operators/main.js:
--------------------------------------------------------------------------------
1 | import { pop, push, remove, shift, sort, unshift } from './array.js'
2 | import { invert } from './boolean.js'
3 | import { set } from './generic.js'
4 | import { add, div, mul, sub } from './number.js'
5 | import { insert, insertstr, slice, slicestr } from './slice.js'
6 | import { replace } from './string.js'
7 |
8 | // All patch operators
9 | export const OPERATORS = {
10 | _set: set,
11 | _add: add,
12 | _sub: sub,
13 | _div: div,
14 | _mul: mul,
15 | _invert: invert,
16 | _replace: replace,
17 | _push: push,
18 | _unshift: unshift,
19 | _pop: pop,
20 | _shift: shift,
21 | _remove: remove,
22 | _sort: sort,
23 | _slicestr: slicestr,
24 | _slice: slice,
25 | _insert: insert,
26 | _insertstr: insertstr,
27 | }
28 |
--------------------------------------------------------------------------------
/docs/dev/coding_style.md:
--------------------------------------------------------------------------------
1 | # Coding style
2 |
3 | We follow the [standard JavaScript style](https://standardjs.com), except for
4 | semicolons and trailing commas.
5 |
6 | Additionally, we enforce a pretty strong functional programming style with
7 | [ESLint](http://eslint.org/), which includes:
8 |
9 | - no complex/big functions/files
10 | - no object-oriented programming
11 | - immutability everywhere
12 | - pure functions
13 | - no complex loops nor structures
14 |
15 | Also we prefer:
16 |
17 | - named arguments over positional
18 | - async/await over raw promises or callbacks
19 | - destructuring
20 | - `...object` over `Object.assign()`
21 |
22 | # Tooling
23 |
24 | We are using [editorconfig](http://editorconfig.org/), so please install the
25 | plugin for your IDE.
26 |
--------------------------------------------------------------------------------
/src/charsets/validate.js:
--------------------------------------------------------------------------------
1 | import iconvLite from 'iconv-lite'
2 |
3 | // Validate `charset` name is valid
4 | export const validateCharset = ({ charset, format }) => {
5 | validateExisting({ charset })
6 | validateWithFormat({ charset, format })
7 | }
8 |
9 | const validateExisting = ({ charset }) => {
10 | if (iconvLite.encodingExists(charset)) {
11 | return
12 | }
13 |
14 | throw new Error(`Unsupported charset: '${charset}'`)
15 | }
16 |
17 | const validateWithFormat = ({ charset, format, format: { title } }) => {
18 | const isValid = format === undefined || format.hasCharset(charset)
19 |
20 | if (isValid) {
21 | return
22 | }
23 |
24 | throw new Error(
25 | `Unsupported charset with a ${title} content type: '${charset}'`,
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/src/databases/features/generic.js:
--------------------------------------------------------------------------------
1 | // Adapter feature `featureName` allows for `args[argName]`
2 | const getGenericValidator = ({ argName, dbName, featureName }) => {
3 | const validator = genericValidator.bind(undefined, { argName, dbName })
4 | return { [featureName]: validator }
5 | }
6 |
7 | const genericValidator = ({ argName, dbName }, { args }) => {
8 | if (args[dbName] === undefined) {
9 | return
10 | }
11 |
12 | return `Must not use argument '${argName}'`
13 | }
14 |
15 | const FEATURES = [
16 | { argName: 'order', dbName: 'order', featureName: 'order' },
17 | { argName: 'page', dbName: 'offset', featureName: 'offset' },
18 | ]
19 |
20 | export const genericValidators = Object.assign(
21 | {},
22 | ...FEATURES.map(getGenericValidator),
23 | )
24 |
--------------------------------------------------------------------------------
/src/middleware/request_response/pagination/output/main.js:
--------------------------------------------------------------------------------
1 | import { getBackwardResponse } from '../backward.js'
2 | import { willPaginate } from '../condition.js'
3 |
4 | import { getPaginationOutput } from './response.js'
5 |
6 | // Add response metadata related to pagination
7 | export const handlePaginationOutput = ({
8 | top,
9 | args,
10 | topargs,
11 | config,
12 | response,
13 | ...rest
14 | }) => {
15 | if (!willPaginate({ top, args, config, ...rest })) {
16 | return
17 | }
18 |
19 | const responseA = getPaginationOutput({
20 | top,
21 | args,
22 | topargs,
23 | config,
24 | response,
25 | })
26 |
27 | const responseB = getBackwardResponse({ args, response: responseA })
28 |
29 | return { response: responseB }
30 | }
31 |
--------------------------------------------------------------------------------
/src/protocols/adapters/http/content_negotiation/compress.js:
--------------------------------------------------------------------------------
1 | import { Negotiator } from 'negotiator'
2 |
3 | import { findAlgo } from '../../../../compress/get.js'
4 |
5 | // Use similar logic as `args.format`, but for `args.compressResponse`
6 | // Uses HTTP header `Accept-Encoding`
7 | export const getCompressResponse = ({ specific: { req } }) => {
8 | const negotiator = new Negotiator(req)
9 | const algos = negotiator.encodings()
10 | const compressResponse = findAlgo(algos)
11 | return compressResponse
12 | }
13 |
14 | // Use similar logic as `args.format`, but for `args.compressRequest`
15 | // Uses HTTP header `Content-Encoding`
16 | export const getCompressRequest = ({
17 | specific: {
18 | req: { headers },
19 | },
20 | }) => headers['content-encoding']
21 |
--------------------------------------------------------------------------------
/src/rpc/adapters/graphql/load/types/object/no_attributes.js:
--------------------------------------------------------------------------------
1 | import { GraphQLString } from 'graphql'
2 |
3 | // GraphQL requires every object field to have attributes,
4 | // which does not always makes sense for us.
5 | // E.g. for `args.data` on 'patch' commands on model whose only attribute
6 | // is 'id'.
7 | // So we add this patch this problem by adding this fake attribute
8 | // when the problem arises.
9 | export const addNoAttributes = ({ fields }) => {
10 | if (Object.keys(fields).length !== 0) {
11 | return fields
12 | }
13 |
14 | return NO_ATTRIBUTES
15 | }
16 |
17 | const NO_ATTRIBUTES = {
18 | no_attributes: {
19 | type: GraphQLString,
20 | description: `This type does not have any attributes.
21 | This is a dummy attribute.`,
22 | },
23 | }
24 |
--------------------------------------------------------------------------------
/src/run/exit/db_close.js:
--------------------------------------------------------------------------------
1 | import { uniq } from '../../utils/functional/uniq.js'
2 |
3 | import { wrapCloseFunc } from './wrapper.js'
4 |
5 | // Attempts to close database connections
6 | // No new connections will be accepted, but we will wait for ongoing ones to end
7 | export const closeDbAdapters = ({ dbAdapters, config, measures }) => {
8 | const dbAdaptersA = Object.values(dbAdapters)
9 | // The same `dbAdapter` can be used for several models
10 | const dbAdaptersB = uniq(dbAdaptersA)
11 |
12 | return dbAdaptersB.map((adapter) =>
13 | eCloseDbAdapter({ type: 'databases', adapter, config, measures }),
14 | )
15 | }
16 |
17 | const closeDbAdapter = ({ adapter: { disconnect } }) => disconnect()
18 |
19 | const eCloseDbAdapter = wrapCloseFunc(closeDbAdapter)
20 |
--------------------------------------------------------------------------------
/src/serverinfo/main.js:
--------------------------------------------------------------------------------
1 | import { pid } from 'node:process'
2 |
3 | import moize from 'moize'
4 |
5 | import { getHostInfo } from './host.js'
6 | import { getProcessInfo } from './process.js'
7 | import { getVersionsInfo } from './versions.js'
8 |
9 | // Retrieve process-specific and host-specific information
10 | const mGetServerinfo = ({ config: { name: processName } = {} }) => {
11 | const host = getHostInfo()
12 | const versions = getVersionsInfo()
13 | const processInfo = getProcessInfo({ host, processName })
14 |
15 | return { host, versions, process: processInfo }
16 | }
17 |
18 | // Speed up memoization because serializing `config` is slow
19 | export const getServerinfo = moize(mGetServerinfo, {
20 | transformArgs: () => pid,
21 | maxSize: 1e3,
22 | })
23 |
--------------------------------------------------------------------------------
/src/bin/instructions.js:
--------------------------------------------------------------------------------
1 | import { availableInstructions } from './available.js'
2 |
3 | // Iterate over `availableOptions` to add all instructions
4 | export const addInstructions = (yargs) =>
5 | yargs.command(availableInstructions.map(getInstruction))
6 |
7 | const getInstruction = ({
8 | command,
9 | aliases,
10 | describe,
11 | arg: { name: argName, ...argOpts },
12 | examples,
13 | options,
14 | }) => ({
15 | command,
16 | aliases,
17 | describe,
18 | builder: (commandYargs) =>
19 | commandYargs
20 | .usage(describe)
21 | .option(options)
22 | .example(examples)
23 | .positional('instruction', INSTRUCTION_OPT)
24 | .positional(argName, argOpts),
25 | })
26 |
27 | const INSTRUCTION_OPT = { type: 'string', default: 'run' }
28 |
--------------------------------------------------------------------------------
/src/rpc/method_check.js:
--------------------------------------------------------------------------------
1 | import { throwPb } from '../errors/props.js'
2 |
3 | // Check if protocol method is allowed for current rpc
4 | export const checkMethod = ({ methods, title }, { method }) => {
5 | if (isAllowedMethod({ methods, method })) {
6 | return
7 | }
8 |
9 | const message = `Invalid protocol with ${title}`
10 | throwPb({
11 | reason: 'METHOD',
12 | message,
13 | extra: { value: method, suggestions: methods },
14 | })
15 | }
16 |
17 | const isAllowedMethod = ({ methods, method }) =>
18 | methods === undefined ||
19 | methods.includes(method) ||
20 | // If only method is allowed by the rpc, but the protocol does not have
21 | // a getMethod(), we do not force specifying `method`
22 | (methods.length === 1 && method === undefined)
23 |
--------------------------------------------------------------------------------
/src/bin/available.js:
--------------------------------------------------------------------------------
1 | import { EXTENSIONS } from '../formats/info.js'
2 |
3 | const runInstruction = {
4 | command: 'run',
5 | aliases: '*',
6 | describe: 'Start the server.',
7 | examples: [['$0 run --protocols.http.port=5001', 'Start the server']],
8 | // This is actually not a positional argument, but meant only
9 | // for --help output
10 | arg: {
11 | name: 'options',
12 | describe: `Any config property, dot-separated.
13 | For example: --protocols.http.port=5001`,
14 | },
15 | options: {
16 | config: {
17 | type: 'string',
18 | describe: `Path to the config file.
19 | By default, will use any file named autoserver.config${EXTENSIONS.join('|')}`,
20 | },
21 | },
22 | }
23 |
24 | export const availableInstructions = [runInstruction]
25 |
--------------------------------------------------------------------------------
/src/rpc/adapters/graphql/load/types/array.js:
--------------------------------------------------------------------------------
1 | import { GraphQLList } from 'graphql'
2 |
3 | export const graphqlArrayTest = ({ arrayWrapped, command, isArray }) => {
4 | // Already wrapped in Array type
5 | if (arrayWrapped) {
6 | return false
7 | }
8 |
9 | // Nested collections' attributes
10 | if (isArray !== undefined) {
11 | return isArray
12 | }
13 |
14 | // Top-level commands
15 | if (command !== undefined) {
16 | return true
17 | }
18 |
19 | // Query|Mutation types
20 | return false
21 | }
22 |
23 | // Array field TGetter
24 | export const graphqlArrayTGetter = (def, opts) => {
25 | const defA = { ...def, arrayWrapped: true }
26 | const subType = opts.getType(defA, opts)
27 | const type = new GraphQLList(subType)
28 | return type
29 | }
30 |
--------------------------------------------------------------------------------
/src/rpc/adapters/graphql/load/types/object/final_fields/args/order.js:
--------------------------------------------------------------------------------
1 | import { GraphQLString } from 'graphql'
2 |
3 | // `order` argument
4 | export const getOrderArgument = ({ command, features }) => {
5 | const canOrder =
6 | ORDER_COMMAND_TYPES.has(command) && features.includes('order')
7 |
8 | if (!canOrder) {
9 | return {}
10 | }
11 |
12 | return ORDER_ARGS
13 | }
14 |
15 | const ORDER_COMMAND_TYPES = new Set(['find'])
16 |
17 | const ORDER_ARGS = {
18 | order: {
19 | type: GraphQLString,
20 | description: `Sort results according to this attribute.
21 | Specify ascending or descending order by appending + or - (default is ascending)
22 | Several attributes can specified, by using a comma-separated list.`,
23 | defaultValue: 'id+',
24 | },
25 | }
26 |
--------------------------------------------------------------------------------
/src/config/reducers/type_validation.js:
--------------------------------------------------------------------------------
1 | import { mapAttrs } from '../helpers.js'
2 |
3 | // Add JSON schema validation based on `type`
4 | const mapAttr = ({ attr, attr: { type, isArray } }) => {
5 | if (!type) {
6 | return
7 | }
8 |
9 | if (!isArray) {
10 | return addSingleValidation(attr)
11 | }
12 |
13 | return addMultipleValidation(attr)
14 | }
15 |
16 | const addSingleValidation = ({ type, validate }) => ({
17 | validate: { ...validate, type },
18 | })
19 |
20 | const addMultipleValidation = ({
21 | type,
22 | validate,
23 | validate: { items = {} },
24 | }) => ({
25 | validate: {
26 | ...validate,
27 | type: 'array',
28 | items: { ...items, type },
29 | },
30 | })
31 |
32 | export const addTypeValidation = mapAttrs.bind(undefined, mapAttr)
33 |
--------------------------------------------------------------------------------
/src/utils/functional/merge.js:
--------------------------------------------------------------------------------
1 | import { isObject } from './type.js'
2 |
3 | // Like Lodash merge() but faster and does not mutate input
4 | export const deepMerge = (objA, objB, ...objects) => {
5 | if (objects.length !== 0) {
6 | const newObjA = deepMerge(objA, objB)
7 | return deepMerge(newObjA, ...objects)
8 | }
9 |
10 | if (objB === undefined) {
11 | return objA
12 | }
13 |
14 | if (!isObjectTypes(objA, objB)) {
15 | return objB
16 | }
17 |
18 | const rObjB = Object.entries(objB).map(([objBKey, objBVal]) => {
19 | const newObjBVal = deepMerge(objA[objBKey], objBVal)
20 | return { [objBKey]: newObjBVal }
21 | })
22 | return Object.assign({}, objA, ...rObjB)
23 | }
24 |
25 | const isObjectTypes = (objA, objB) => isObject(objA) && isObject(objB)
26 |
--------------------------------------------------------------------------------
/src/databases/adapters/memory/query/main.js:
--------------------------------------------------------------------------------
1 | import { setTimeout } from 'node:timers/promises'
2 |
3 | import { deleteMany } from './delete.js'
4 | import { find } from './find/main.js'
5 | import { upsert } from './upsert.js'
6 |
7 | // CRUD commands
8 | export const query = async ({
9 | collname,
10 | command,
11 | filter,
12 | deletedIds,
13 | newData,
14 | order,
15 | limit,
16 | offset,
17 | connection,
18 | }) => {
19 | // Simulate asynchronousity
20 | await setTimeout(0)
21 |
22 | const collection = connection[collname]
23 |
24 | return COMMANDS[command]({
25 | collection,
26 | filter,
27 | deletedIds,
28 | newData,
29 | order,
30 | limit,
31 | offset,
32 | })
33 | }
34 |
35 | const COMMANDS = { find, delete: deleteMany, upsert }
36 |
--------------------------------------------------------------------------------
/src/middleware/request_response/response_validation.js:
--------------------------------------------------------------------------------
1 | import { throwPb } from '../../errors/props.js'
2 |
3 | // Check output, for the errors that should not happen,
4 | // i.e. server-side (e.g. 500)
5 | // In short: response should be an array of objects
6 | export const responseValidation = ({ response: { data, metadata } }) => {
7 | if (!data) {
8 | const message = "'response.data' should be defined"
9 | throwPb({ message, reason: 'ENGINE' })
10 | }
11 |
12 | if (!Array.isArray(data)) {
13 | const message = `'response.data' should be an array, not '${data}'`
14 | throwPb({ message, reason: 'ENGINE' })
15 | }
16 |
17 | if (!metadata) {
18 | const message = "'response.metadata' should be defined"
19 | throwPb({ message, reason: 'ENGINE' })
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/docs/client/arguments/params.md:
--------------------------------------------------------------------------------
1 | # Client-specific parameters
2 |
3 | Clients can specify
4 | [parameters](../../server/configuration/functions.md#parameters) on any specific
5 | request using the `params` [argument](../rpc/README.md#rpc) with an object
6 | value.
7 |
8 | [Parameters](../../server/configuration/functions.md#parameters) are custom
9 | pieces of information passed to the server. How those parameters are interpreted
10 | is server-specific.
11 |
12 | ```graphql
13 | query {
14 | find_users(id: "1", params: { password: "admin" }) {
15 | id
16 | }
17 | }
18 | ```
19 |
20 | They can also be set using the [protocol header](../protocols/README.md)
21 | `params`, with a JSON object as value. For example, with HTTP:
22 |
23 | ```HTTP
24 | X-Autoserver-Params: {"password": "admin"}
25 | ```
26 |
--------------------------------------------------------------------------------
/src/middleware/database/response.js:
--------------------------------------------------------------------------------
1 | // Retrieves database return value
2 | // Only `find` command use database return value.
3 | // `create`, `patch` and `upsert` assumes database does not modify input,
4 | // i.e. reuse `args.newData`
5 | // `delete` reuse data before deletion, i.e. use `args.currentData`
6 | export const getDbResponse = ({
7 | dbData,
8 | args: { currentData, newData },
9 | command,
10 | }) => {
11 | const dataInput = { dbData, newData, currentData }
12 | const data = dataInput[RESPONSE_MAP[command]]
13 | const metadata = {}
14 | const response = { data, metadata }
15 |
16 | return { response }
17 | }
18 |
19 | const RESPONSE_MAP = {
20 | find: 'dbData',
21 | create: 'newData',
22 | upsert: 'newData',
23 | patch: 'newData',
24 | delete: 'currentData',
25 | }
26 |
--------------------------------------------------------------------------------
/src/compress/get.js:
--------------------------------------------------------------------------------
1 | import { getAdapter } from '../adapters/get.js'
2 |
3 | import { compressAdapters } from './wrap.js'
4 |
5 | // Retrieves compression adapter
6 | export const getAlgo = (algo = 'identity') => {
7 | const algoA = algo.trim().toLowerCase()
8 |
9 | const compressAdapter = getAdapter({
10 | adapters: compressAdapters,
11 | key: algoA,
12 | name: 'compression algorithm',
13 | })
14 | return compressAdapter
15 | }
16 |
17 | // Find compression algorithm is among the adapters.
18 | // Follows key orders, i.e. priority set by this module.
19 | export const findAlgo = (algos) =>
20 | Object.keys(compressAdapters).find((algo) => algos.includes(algo))
21 |
22 | export const getAlgos = () => Object.keys(compressAdapters)
23 |
24 | export const DEFAULT_ALGO = getAlgo('identity')
25 |
--------------------------------------------------------------------------------
/src/protocols/request/method.js:
--------------------------------------------------------------------------------
1 | import { throwPb } from '../../errors/props.js'
2 |
3 | import { validateString } from './validate.js'
4 |
5 | export const parseMethod = ({
6 | protocolAdapter,
7 | protocolAdapter: { getMethod },
8 | specific,
9 | }) => {
10 | if (getMethod === undefined) {
11 | return
12 | }
13 |
14 | const method = getMethod({ specific })
15 |
16 | validateString(method, 'method', protocolAdapter)
17 | validateMethod({ method })
18 |
19 | return { method }
20 | }
21 |
22 | const validateMethod = ({ method }) => {
23 | if (method === undefined || METHODS.has(method)) {
24 | return
25 | }
26 |
27 | throwPb({ reason: 'METHOD', extra: { value: method, suggestions: METHODS } })
28 | }
29 |
30 | const METHODS = new Set(['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE'])
31 |
--------------------------------------------------------------------------------
/src/utils/string.js:
--------------------------------------------------------------------------------
1 | import underscoreString from 'underscore.string'
2 |
3 | // Turn ['a', 'b', 'c'] into 'a, b or c'
4 | export const getWordsList = (
5 | words,
6 | { op = 'or', quotes = false, json = false } = {},
7 | ) => {
8 | if (words.length === 0) {
9 | return ''
10 | }
11 |
12 | const wordsA = jsonStringify(words, { json })
13 | const wordsB = quoteWords(wordsA, { quotes })
14 | const wordsC = underscoreString.toSentence(wordsB, ', ', ` ${op} `)
15 | return wordsC
16 | }
17 |
18 | const jsonStringify = (words, { json }) => {
19 | if (!json) {
20 | return words
21 | }
22 |
23 | return words.map(JSON.stringify)
24 | }
25 |
26 | const quoteWords = (words, { quotes }) => {
27 | if (!quotes) {
28 | return words
29 | }
30 |
31 | return words.map((word) => `'${word}'`)
32 | }
33 |
--------------------------------------------------------------------------------
/docs/server/databases/mongodb.md:
--------------------------------------------------------------------------------
1 | # MongoDB options
2 |
3 | The following [options](README.md#options) are available:
4 |
5 | - `hostname`, `port` (defaults to `localhost` and `27017`): if you are targeting
6 | a cluster or a replica set, you must specify an array of hostnames and/or an
7 | array of ports.
8 | - `username`, `password`: optional authentication information
9 | - `dbname` (defaults to `data`): MongoDB database name
10 | - `opts`: extra options passed to the
11 | [MongoDB driver](http://mongodb.github.io/node-mongodb-native/2.2/reference/connecting/connection-settings/).
12 |
13 | ```yml
14 | databases:
15 | mongodb:
16 | hostname: my_host
17 | port: 28000
18 | username: my_user
19 | password: my_password
20 | dbname: my_database_name
21 | opts:
22 | wtimeoutMS: 2000
23 | ```
24 |
--------------------------------------------------------------------------------
/src/formats/adapters/raw.js:
--------------------------------------------------------------------------------
1 | import { Buffer } from 'node:buffer'
2 |
3 | const { isBuffer } = Buffer
4 |
5 | // Parses a raw value
6 | const parse = ({ content }) => content
7 |
8 | // Serializes any value to a string
9 | const serialize = ({ content }) => {
10 | if (typeof content === 'string') {
11 | return content
12 | }
13 |
14 | if (isBuffer(content)) {
15 | return content.toString()
16 | }
17 |
18 | return JSON.stringify(content, undefined, 2)
19 | }
20 |
21 | // Means this is not a structured type, like media types,
22 | // and unlike JSON or YAML
23 | // This won't be parsed (i.e. returned as is), and will use 'binary' charset
24 | export const raw = {
25 | name: 'raw',
26 | title: 'raw',
27 | extensions: [],
28 | mimes: [],
29 | jsonCompat: [],
30 | parse,
31 | serialize,
32 | }
33 |
--------------------------------------------------------------------------------
/src/protocols/request/content_negotiation/charset.js:
--------------------------------------------------------------------------------
1 | import { getCharset, getCharsets } from '../../../charsets/main.js'
2 | import { addGenPbHandler } from '../../../errors/handler.js'
3 |
4 | // Retrieve charset asked by client for the request and response payload
5 | export const getCharsetFunc = ({ queryvars, charset, format }) => {
6 | // E.g. ?charset query variable or charset in Content-Type HTTP header
7 | const charsetA = queryvars.charset || charset
8 | const charsetB = eGetCharset(charsetA, { format })
9 | return charsetB
10 | }
11 |
12 | const getExtra = (charset) => {
13 | const suggestions = getCharsets()
14 | return { kind: 'charset', value: [charset], suggestions }
15 | }
16 |
17 | const eGetCharset = addGenPbHandler(getCharset, {
18 | reason: 'RESPONSE_NEGOTIATION',
19 | extra: getExtra,
20 | })
21 |
--------------------------------------------------------------------------------
/src/utils/functional/immutable.js:
--------------------------------------------------------------------------------
1 | // Deeply Object.freeze() over an object.
2 | // Since linting enforces immutability, we only need to (and should) perform
3 | // this on values that are passed to library caller.
4 | // This directy mutates the argument for performance reasons
5 | export const makeImmutable = (val) => {
6 | // Avoid infinite recursions
7 | const isFrozen = Object.isFrozen(val)
8 |
9 | if (isFrozen) {
10 | return val
11 | }
12 |
13 | Object.freeze(val)
14 |
15 | freezeChildren(val)
16 | }
17 |
18 | // Non-plain objects must be directly mutated
19 | const freezeChildren = (obj) => {
20 | if (obj === null || typeof obj !== 'object') {
21 | return
22 | }
23 |
24 | // eslint-disable-next-line fp/no-loops
25 | for (const val of Object.values(obj)) {
26 | makeImmutable(val)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/formats/content.js:
--------------------------------------------------------------------------------
1 | import { applyCompatParse, applyCompatSerialize } from './compat.js'
2 |
3 | // Generic parser, delegating to the format specified in `format`
4 | export const parseContent = async (
5 | { parse, jsonCompat },
6 | content,
7 | { path, compat = true } = {},
8 | ) => {
9 | const contentA = await parse({ content, path })
10 |
11 | if (!compat) {
12 | return contentA
13 | }
14 |
15 | const contentB = applyCompatParse({ jsonCompat, content: contentA })
16 | return contentB
17 | }
18 |
19 | // Generic serializer, delegating to the format specified in `format`
20 | export const serializeContent = async ({ serialize, jsonCompat }, content) => {
21 | const contentA = applyCompatSerialize({ jsonCompat, content })
22 | const contentB = await serialize({ content: contentA })
23 | return contentB
24 | }
25 |
--------------------------------------------------------------------------------
/src/functions/test.js:
--------------------------------------------------------------------------------
1 | import { tokenizeInlineFunc } from './tokenize.js'
2 |
3 | // Test whether a value is inline function
4 | export const isInlineFunc = (inlineFunc) =>
5 | testInlineFunc(inlineFunc) === 'InlineFunc'
6 |
7 | // Test whether a value is almost inline function,
8 | // except opening parenthesis is escaped
9 | export const isEscapedInlineFunc = (inlineFunc) =>
10 | testInlineFunc(inlineFunc) === 'Escaped'
11 |
12 | const testInlineFunc = (inlineFunc) => {
13 | if (typeof inlineFunc !== 'string') {
14 | return 'NotAString'
15 | }
16 |
17 | const parsedInlineFunc = tokenizeInlineFunc(inlineFunc)
18 |
19 | if (parsedInlineFunc === null) {
20 | return 'NoParenthesis'
21 | }
22 |
23 | if (parsedInlineFunc.groups.escape !== '') {
24 | return 'Escaped'
25 | }
26 |
27 | return 'InlineFunc'
28 | }
29 |
--------------------------------------------------------------------------------
/src/rpc/adapters/graphql/load/type.js:
--------------------------------------------------------------------------------
1 | import { randomUUID } from 'node:crypto'
2 |
3 | import { mapValues } from '../../../../utils/functional/map.js'
4 |
5 | import { getTypeGetter } from './types/main.js'
6 |
7 | // Builds query|mutation type
8 | export const getTopTypes = ({ topDefs }) => {
9 | const graphqlSchemaId = randomUUID()
10 | // `getType`: recursion, while avoiding files circular dependencies
11 | const opts = {
12 | inputObjectType: 'type',
13 | getType,
14 | graphqlSchemaId,
15 | }
16 |
17 | return mapValues(topDefs, (topDef) => getType(topDef, { ...opts, topDef }))
18 | }
19 |
20 | // Retrieves the GraphQL type for a given config definition
21 | const getType = (def, opts) => {
22 | const typeGetter = getTypeGetter(def, opts)
23 | const type = typeGetter.value(def, opts)
24 | return type
25 | }
26 |
--------------------------------------------------------------------------------
/src/middleware/action/unknown_attrs/all.js:
--------------------------------------------------------------------------------
1 | import { throwError } from '../../../errors/main.js'
2 |
3 | // Validate correct usage of special key 'all'
4 | export const validateAllAttr = ({
5 | action: {
6 | args: { select },
7 | collname,
8 | },
9 | config: { collections },
10 | }) => {
11 | if (select === undefined) {
12 | return
13 | }
14 |
15 | const hasAllAttr = select.includes('all')
16 |
17 | if (!hasAllAttr) {
18 | return
19 | }
20 |
21 | const keyA = select
22 | .filter((key) => key !== 'all')
23 | .find((key) => collections[collname].attributes[key].target === undefined)
24 |
25 | if (keyA === undefined) {
26 | return
27 | }
28 |
29 | const message = `Argument 'select' cannot target both 'all' and '${keyA}' attributes`
30 | throwError(message, { reason: 'VALIDATION' })
31 | }
32 |
--------------------------------------------------------------------------------
/src/databases/adapters/mongodb/query/upsert.js:
--------------------------------------------------------------------------------
1 | // Modify models
2 | export const upsert = ({ collection, newData }) => {
3 | const func = newData.length === 1 ? upsertOne : upsertMany
4 | return func({ collection, newData })
5 | }
6 |
7 | const upsertOne = ({ collection, newData: [data] }) => {
8 | const { _id: id } = data
9 | return collection.replaceOne({ _id: id }, data, { upsert: true })
10 | }
11 |
12 | const upsertMany = async ({ collection, newData }) => {
13 | const bulkCommands = newData.map((replacement) =>
14 | getBulkCommand({ replacement }),
15 | )
16 | const result = await collection.bulkWrite(bulkCommands)
17 | return { result }
18 | }
19 |
20 | const getBulkCommand = ({ replacement }) => {
21 | const { _id: id } = replacement
22 | return { replaceOne: { filter: { _id: id }, replacement, upsert: true } }
23 | }
24 |
--------------------------------------------------------------------------------
/src/middleware/request_response/pagination/encoding/main.js:
--------------------------------------------------------------------------------
1 | import { base64UrlDecode, base64UrlEncode } from '../../../../utils/base64.js'
2 |
3 | import { convertUndefined } from './convert_undefined.js'
4 | import { addNameShortcuts, removeNameShortcuts } from './minify_names.js'
5 |
6 | // Encode token from a usable object to a short opaque base64 token
7 | // Make sure token is small by minifying it
8 | export const encode = ({ token }) =>
9 | encoders.reduce((tokenA, encoder) => encoder(tokenA), token)
10 |
11 | const encoders = [
12 | convertUndefined,
13 | addNameShortcuts,
14 | JSON.stringify,
15 | base64UrlEncode,
16 | ]
17 |
18 | export const decode = ({ token }) =>
19 | decoders.reduce((tokenA, decoder) => decoder(tokenA), token)
20 |
21 | // Inverse
22 | const decoders = [base64UrlDecode, JSON.parse, removeNameShortcuts]
23 |
--------------------------------------------------------------------------------
/src/rpc/adapters/graphql/load/name.js:
--------------------------------------------------------------------------------
1 | import underscoreString from 'underscore.string'
2 |
3 | // Returns top-level command name, e.g. `find_collection` or `delete_collection`
4 | export const getCommandName = ({ clientCollname, command }) =>
5 | `${command}_${clientCollname}`
6 |
7 | // Returns type name:
8 | // - 'Model' for normal return types
9 | // - 'CommandCollData' and 'CommandCollFilter' for `args.data|filter` types
10 | export const getTypeName = ({
11 | def: { clientCollname, command },
12 | opts: { inputObjectType = 'type' } = {},
13 | }) => {
14 | const typeName =
15 | inputObjectType === 'type'
16 | ? clientCollname
17 | : `${command}_${clientCollname}_${inputObjectType}`
18 | const typeNameA = underscoreString.camelize(
19 | underscoreString.capitalize(typeName),
20 | )
21 | return typeNameA
22 | }
23 |
--------------------------------------------------------------------------------
/src/rpc/adapters/graphql/parse/definition/populate.js:
--------------------------------------------------------------------------------
1 | import { uniq } from '../../../../../utils/functional/uniq.js'
2 |
3 | // Retrieve `rpcDef.args.populate` using GraphQL selection sets
4 | export const addPopulate = ({ args, args: { select }, commandName }) => {
5 | if (!commandName.startsWith('find_')) {
6 | return args
7 | }
8 |
9 | const selects = select
10 | .split(',')
11 | .map((selectA) => selectA.replace(PARENT_SELECT_REGEXP, ''))
12 | .filter((selectA) => selectA !== '')
13 | const selectsA = uniq(selects)
14 |
15 | if (selectsA.length === 0) {
16 | return args
17 | }
18 |
19 | const populate = selectsA.join(',')
20 |
21 | const argsA = { ...args, populate }
22 | return argsA
23 | }
24 |
25 | // Keep only parent path, e.g. 'parent.child' -> 'parent'
26 | const PARENT_SELECT_REGEXP = /\.?[^.]+$/u
27 |
--------------------------------------------------------------------------------
/docs/server/configuration/references.md:
--------------------------------------------------------------------------------
1 | # References
2 |
3 | The [configuration file](configuration.md#configuration-file) can be broken down
4 | into several files by referring to local files with
5 | [references](https://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03). Those
6 | are simple objects with a single `$ref` property pointing to the file.
7 |
8 | ```yml
9 | collections:
10 | example_collection:
11 | $ref: example_collection.yml
12 | ```
13 |
14 | References are deeply merged with their siblings, which allows you to extend a
15 | configuration from another configuration.
16 |
17 | ```yml
18 | $ref: base_config.yml
19 | collections: ...
20 | params: ...
21 | ```
22 |
23 | # Node.js modules
24 |
25 | Node.js modules can be imported by appending `.node`.
26 |
27 | ```yml
28 | params:
29 | lodash:
30 | $ref: lodash.node
31 | ```
32 |
--------------------------------------------------------------------------------
/src/protocols/adapters/http/method.js:
--------------------------------------------------------------------------------
1 | // Retrieves HTTP method, but protocol-agnostic
2 | export const getMethod = ({
3 | specific: {
4 | req: { method },
5 | },
6 | }) => getAgnosticMethod({ method })
7 |
8 | export const getAgnosticMethod = ({ method }) => {
9 | if (typeof method !== 'string') {
10 | return method
11 | }
12 |
13 | const methodA = METHODS_MAP[method.toUpperCase()]
14 |
15 | if (methodA === undefined) {
16 | return method
17 | }
18 |
19 | return methodA
20 | }
21 |
22 | // This looks strange, but this is just to enforce the fact that the HTTP
23 | // method (the key) is conceptually different from the protocol-agnostic method
24 | // (the value)
25 | const METHODS_MAP = {
26 | GET: 'GET',
27 | HEAD: 'HEAD',
28 | POST: 'POST',
29 | PUT: 'PUT',
30 | PATCH: 'PATCH',
31 | DELETE: 'DELETE',
32 | }
33 |
--------------------------------------------------------------------------------
/src/run/exit/wrapper.js:
--------------------------------------------------------------------------------
1 | import { monitor } from '../../perf/helpers.js'
2 |
3 | import { addExitHandler } from './error.js'
4 | import { emitMessageEvent } from './message.js'
5 |
6 | // Add event handling, message event and monitoring capabilities to the function
7 | export const wrapCloseFunc = (func) => {
8 | const funcA = closeFunc.bind(undefined, func)
9 |
10 | const eFunc = addExitHandler(funcA)
11 |
12 | const mFunc = monitor(eFunc, getEventLabel, 'main')
13 | return mFunc
14 | }
15 |
16 | const closeFunc = async (func, opts) => {
17 | await emitMessageEvent({ ...opts, step: 'start' })
18 |
19 | await func(opts)
20 |
21 | await emitMessageEvent({ ...opts, step: 'end' })
22 |
23 | // Exit status
24 | return { [opts.adapter.name]: true }
25 | }
26 |
27 | const getEventLabel = ({ type, adapter: { name } }) => `${type}.${name}`
28 |
--------------------------------------------------------------------------------
/src/middleware/action/rollback/failure.js:
--------------------------------------------------------------------------------
1 | import { isError } from '../../../errors/main.js'
2 |
3 | // Rethrow original error
4 | export const rethrowFailure = ({ failedActions: [error], results }) => {
5 | const errorA = addRollbackFailures({ error, results })
6 |
7 | throw errorA
8 | }
9 |
10 | // If rollback itself fails, give up and add rollback error to error response,
11 | // as `error.rollback_failures`
12 | const addRollbackFailures = ({ error, results }) => {
13 | const rollbackFailures = results.filter((result) =>
14 | isError({ error: result }),
15 | )
16 |
17 | if (rollbackFailures.length === 0) {
18 | return error
19 | }
20 |
21 | const rollbackFailuresA = rollbackFailures
22 | .map(({ message }) => message)
23 | .join('\n')
24 |
25 | error.extra.rollback_failures = rollbackFailuresA
26 | return error
27 | }
28 |
--------------------------------------------------------------------------------
/src/rpc/adapters/graphql/load/types/object/final_fields/main.js:
--------------------------------------------------------------------------------
1 | import { getArgs } from './args/main.js'
2 | import { getDefaultValue } from './default.js'
3 |
4 | // Retrieves a GraphQL field info for a given config definition,
5 | // i.e. an object that can be passed to new
6 | // GraphQLObjectType({ fields })
7 | // Includes return type, resolve function, arguments, etc.
8 | export const getFinalField = (def, opts) => {
9 | const type = opts.getType(def, opts)
10 |
11 | const args = getArgs(def, opts)
12 |
13 | const defaultValue = getDefaultValue(def, opts)
14 |
15 | // `commandDescription` will only be used with top-level actions
16 | const description = def.commandDescription || def.description
17 | const { deprecation_reason: deprecationReason } = def
18 |
19 | return { type, args, defaultValue, description, deprecationReason }
20 | }
21 |
--------------------------------------------------------------------------------
/docs/client/arguments/renaming.md:
--------------------------------------------------------------------------------
1 | # Renaming attributes in response
2 |
3 | The `rename` [argument](../rpc/README.md#rpc) can be used to rename attributes
4 | in the response.
5 |
6 | It is a comma-separated list of `name:different_name`.
7 | [Nested attributes](../request/relations.md#populating-nested-collections) can
8 | be specified using a dot notation.
9 |
10 | ```HTTP
11 | GET /rest/users/1?rename=name:different_name
12 | ```
13 |
14 | will respond with:
15 |
16 | ```json
17 | {
18 | "data": {
19 | "different_name": "Anthony"
20 | }
21 | }
22 | ```
23 |
24 | instead of:
25 |
26 | ```json
27 | {
28 | "data": {
29 | "name": "Anthony"
30 | }
31 | }
32 | ```
33 |
34 | [GraphQL](../rpc/graphql.md#selection-population-and-renaming) does not need the
35 | `rename` [argument](../rpc/README.md#rpc) since it natively uses selection
36 | fields.
37 |
--------------------------------------------------------------------------------
/src/run/main.js:
--------------------------------------------------------------------------------
1 | import { addErrorHandler } from '../errors/handler.js'
2 | import { monitoredReduce } from '../perf/helpers.js'
3 |
4 | import { handleStartupError } from './error.js'
5 | import { startupSteps } from './steps.js'
6 |
7 | // Start server for each protocol
8 | export const run = async ({
9 | measures = [],
10 | config: configPath,
11 | ...config
12 | } = {}) => {
13 | // Run each startup step
14 | const { startPayload } = await monitoredReduce({
15 | funcs: eStartupSteps,
16 | initialInput: { measures, configPath, config },
17 | mapResponse: (input, newInput) => ({ ...input, ...newInput }),
18 | category: 'main',
19 | })
20 |
21 | return startPayload
22 | }
23 |
24 | // Add startup error handler
25 | const eStartupSteps = startupSteps.map((startupStep) =>
26 | addErrorHandler(startupStep, handleStartupError),
27 | )
28 |
--------------------------------------------------------------------------------
/src/protocols/adapters/http/headers.js:
--------------------------------------------------------------------------------
1 | import { includeKeys } from 'filter-obj'
2 |
3 | import { mapKeys, mapValues } from '../../../utils/functional/map.js'
4 | import { transtype } from '../../../utils/transtype.js'
5 |
6 | // Returns a request's application-specific HTTP headers, normalized lowercase.
7 | // At the moment, only keeps X-Autoserver-Params header
8 | export const getHeaders = ({
9 | specific: {
10 | req: { headers },
11 | },
12 | }) => {
13 | const headersA = includeKeys(headers, HEADER_NAMES)
14 | const headersB = mapKeys(headersA, (value, name) =>
15 | name.replace(ARGS_REGEXP, '$2'),
16 | )
17 | const headersC = mapValues(headersB, transtype)
18 | return headersC
19 | }
20 |
21 | // Allowed headers
22 | const HEADER_NAMES = ['x-autoserver-params']
23 |
24 | // Remove prefix
25 | const ARGS_REGEXP = /^(x-autoserver-)(.+)$/u
26 |
--------------------------------------------------------------------------------
/src/rpc/transform.js:
--------------------------------------------------------------------------------
1 | import { isType } from '../content_types.js'
2 |
3 | // Transform a response according to rpc syntax
4 | // Differs depending on whether the response is an error or a success
5 | export const transformResponse = (
6 | { transformError, transformSuccess },
7 | { response, response: { type, content }, mInput },
8 | ) => {
9 | if (shouldTransformError({ type, transformError })) {
10 | return transformError({ ...mInput, response })
11 | }
12 |
13 | if (shouldTransformSuccess({ type, transformSuccess })) {
14 | return transformSuccess({ ...mInput, response })
15 | }
16 |
17 | return content
18 | }
19 |
20 | const shouldTransformError = ({ type, transformError }) =>
21 | isType(type, 'error') && transformError
22 |
23 | const shouldTransformSuccess = ({ type, transformSuccess }) =>
24 | isType(type, 'model') && transformSuccess
25 |
--------------------------------------------------------------------------------
/src/config/reducers/type.js:
--------------------------------------------------------------------------------
1 | import { mapAttrs } from '../helpers.js'
2 |
3 | // From `type: string[]` or `type: my_coll`
4 | // to `type: string, isArray: true` or `target: my_coll, isArray: false`
5 | const mapAttr = ({ attr }) => {
6 | const [, rawType, brackets] = TYPE_REGEXP.exec(attr.type)
7 | const isArray = brackets !== undefined
8 | const isColl = !NON_COLL_TYPES.has(rawType)
9 |
10 | if (isColl) {
11 | return { type: undefined, target: rawType, isArray }
12 | }
13 |
14 | return { type: rawType, isArray }
15 | }
16 |
17 | // Parse 'type[]' to ['type', '[]'] and 'type' to ['type', '']
18 | const TYPE_REGEXP = /([^[]*)(\[\])?$/u
19 |
20 | const NON_COLL_TYPES = new Set([
21 | 'array',
22 | 'object',
23 | 'string',
24 | 'number',
25 | 'integer',
26 | 'null',
27 | 'boolean',
28 | ])
29 |
30 | export const normalizeType = mapAttrs.bind(undefined, mapAttr)
31 |
--------------------------------------------------------------------------------
/src/errors/reasons/message.js:
--------------------------------------------------------------------------------
1 | import pluralize from 'pluralize'
2 |
3 | import { getWordsList } from '../../utils/string.js'
4 |
5 | // Try to make error messages start the same way when referring to models
6 | export const getModels = ({ ids, op = 'and', collection } = {}) => {
7 | if (collection === undefined) {
8 | return 'Those models'
9 | }
10 |
11 | if (ids === undefined) {
12 | return `Those '${collection}' models`
13 | }
14 |
15 | const idsA = getWordsList(ids, { op, quotes: true })
16 | const modelsStr = pluralize('model', ids.length)
17 | const models = `The '${collection}' ${modelsStr} with 'id' ${idsA}`
18 | return models
19 | }
20 |
21 | // Add prefix common to all adapter-related errors
22 | export const getAdapterMessage = ({ adapter }) => {
23 | if (adapter === undefined) {
24 | return
25 | }
26 |
27 | return `In the adapter '${adapter}'`
28 | }
29 |
--------------------------------------------------------------------------------
/src/formats/file.js:
--------------------------------------------------------------------------------
1 | import { readFile, writeFile } from 'node:fs/promises'
2 |
3 | import { parseContent, serializeContent } from './content.js'
4 |
5 | // Loads any of the supported formats
6 | // This is abstracted to allow easily adding new formats.
7 | // This might throw for many different reasons, e.g. wrong syntax,
8 | // or cannot access file (does not exist or no permissions)
9 | export const parseFile = async (format, path, { compat }) => {
10 | const content = await readFile(path, 'utf8')
11 |
12 | const contentA = await parseContent(format, content, { path, compat })
13 | return contentA
14 | }
15 |
16 | // Persist file, using any of the supported formats
17 | export const serializeFile = async (format, path, content) => {
18 | const contentA = await serializeContent(format, content)
19 |
20 | const contentB = await writeFile(path, contentA)
21 | return contentB
22 | }
23 |
--------------------------------------------------------------------------------
/src/validation/validator.js:
--------------------------------------------------------------------------------
1 | import Ajv from 'ajv'
2 | import ajvFormats from 'ajv-formats'
3 | import ajvKeywords from 'ajv-keywords'
4 |
5 | export const getValidator = () => {
6 | const ajv = new Ajv(AJV_OPTIONS)
7 | ajvKeywords(ajv, CUSTOM_KEYWORDS)
8 | ajvFormats(ajv)
9 | return ajv
10 | }
11 |
12 | // Add JSON keywords:
13 | // - typeof: allows checking for `typeof function`
14 | const CUSTOM_KEYWORDS = ['typeof']
15 |
16 | // Intercepts when ajv uses console.* and throw exceptions instead
17 | const loggerError = (...args) => {
18 | const message = args.join(' ')
19 | throw new Error(message)
20 | }
21 |
22 | const logger = { log: loggerError, warn: loggerError, error: loggerError }
23 |
24 | const AJV_OPTIONS = {
25 | allErrors: true,
26 | $data: true,
27 | verbose: true,
28 | logger,
29 | multipleOfPrecision: 9,
30 | // TODO: remove
31 | allowUnionTypes: true,
32 | }
33 |
--------------------------------------------------------------------------------
/src/databases/adapters/memory/disconnect.js:
--------------------------------------------------------------------------------
1 | import { addGenPbHandler } from '../../../errors/handler.js'
2 | import { getByExt } from '../../../formats/get.js'
3 | import { getRef } from '../../../json_refs/ref_path.js'
4 |
5 | // Stops connection
6 | // Persist back to file, unless database adapter option `save` is false
7 | export const disconnect = async ({ options: { save, data }, connection }) => {
8 | if (!save) {
9 | return
10 | }
11 |
12 | // Reuse the same file that was used during loading
13 | const path = getRef(data)
14 | const format = eGetByExt({ path })
15 |
16 | await format.serializeFile(path, connection)
17 | }
18 |
19 | const eGetByExt = addGenPbHandler(getByExt, {
20 | message: ({ path }) =>
21 | `Memory database file format is not supported: '${path}'`,
22 | reason: 'CONFIG_RUNTIME',
23 | extra: ({ path }) => ({ path: 'databases.memory.data', value: path }),
24 | })
25 |
--------------------------------------------------------------------------------
/src/patch/ref.js:
--------------------------------------------------------------------------------
1 | import { throwError } from '../errors/main.js'
2 |
3 | import { parseRef } from './ref_parsing.js'
4 | import { postValidate } from './validate/main.js'
5 |
6 | // Replaces model.ATTR in simple patch operations (i.e. with no operators)
7 | export const replaceSimpleRef = ({ ref, attributes, datum, commandpath }) => {
8 | if (attributes[ref] !== undefined) {
9 | return datum[ref]
10 | }
11 |
12 | const message = `At '${commandpath.join('.')}': attribute '${ref}' is unknown`
13 | throwError(message, { reason: 'VALIDATION' })
14 | }
15 |
16 | // Replaces model.ATTR when patch operation is applied
17 | export const replaceRef = ({ opVal, datum, ...rest }) => {
18 | const ref = parseRef(opVal)
19 |
20 | if (ref === undefined) {
21 | return opVal
22 | }
23 |
24 | const opValA = datum[ref]
25 |
26 | postValidate({ opVal: opValA, ...rest })
27 |
28 | return opValA
29 | }
30 |
--------------------------------------------------------------------------------
/examples/main.js:
--------------------------------------------------------------------------------
1 | import { stdout } from 'node:process'
2 | import { fileURLToPath } from 'node:url'
3 | import { inspect } from 'node:util'
4 |
5 | import { run } from 'autoserver'
6 |
7 | const CONFIG = fileURLToPath(new URL('autoserver.config.yml', import.meta.url))
8 |
9 | // Set default console log printing
10 | const setDefaultDebug = () => {
11 | // eslint-disable-next-line fp/no-mutation
12 | inspect.defaultOptions = {
13 | colors: true,
14 | // eslint-disable-next-line unicorn/no-null
15 | depth: null,
16 | breakLength: stdout.columns || COLUMNS_WIDTH,
17 | }
18 | }
19 |
20 | const COLUMNS_WIDTH = 80
21 |
22 | const startServer = async () => {
23 | try {
24 | const { protocols, exit } = await run({ config: CONFIG })
25 | return { protocols, exit }
26 | } catch {
27 | console.log('Startup error')
28 | }
29 | }
30 |
31 | setDefaultDebug()
32 |
33 | await startServer()
34 |
--------------------------------------------------------------------------------
/src/filter/operators/in_nin.js:
--------------------------------------------------------------------------------
1 | import { throwAttrValError } from '../error.js'
2 |
3 | import { parseAsIs, validateNotArray, validateSameType } from './common.js'
4 |
5 | const validateInNin = ({ value, type, attr, throwErr }) => {
6 | validateNotArray({ type, attr, throwErr })
7 |
8 | if (!Array.isArray(value)) {
9 | throwAttrValError({ type, throwErr }, 'an array')
10 | }
11 |
12 | value.forEach((val) => {
13 | validateSameType({ value: val, type, attr, throwErr })
14 | })
15 | }
16 |
17 | // `{ attribute: { _in: [...] } }`
18 | const evalIn = ({ attr, value }) => value.includes(attr)
19 |
20 | // `{ attribute: { _nin: [...] } }`
21 | const evalNin = ({ attr, value }) => !value.includes(attr)
22 |
23 | export const inOperator = {
24 | parse: parseAsIs,
25 | validate: validateInNin,
26 | eval: evalIn,
27 | }
28 | export const nin = { parse: parseAsIs, validate: validateInNin, eval: evalNin }
29 |
--------------------------------------------------------------------------------
/docs/server/data_model/relations.md:
--------------------------------------------------------------------------------
1 | # Relations
2 |
3 | Collections can refer to each other by using the other collection's name as the
4 | `attribute.type`
5 | [configuration property](../configuration/configuration.md#properties), either
6 | as a scalar value or an array, for one-to-one or one-to-many relationship.
7 |
8 | ```yml
9 | collections:
10 | users:
11 | attributes:
12 | friends:
13 | type: users[]
14 | manager:
15 | type: users
16 | ```
17 |
18 | Models can nest themselves, i.e. be recursive.
19 |
20 | Nested attributes are using the `id` attribute of the collection they refer to.
21 |
22 | Nested collections can be deeply
23 | [populated](../../client/request/relations.md#populating-nested-collections),
24 | [modified](../../client/request/relations.md#modifying-nested-collections) and
25 | [deleted](../../client/request/relations.md#deleting-nested-collections) by
26 | client.
27 |
--------------------------------------------------------------------------------
/docs/server/databases/memorydb.md:
--------------------------------------------------------------------------------
1 | # In-memory database
2 |
3 | The in-memory [database](README.md) keeps all data in RAM, and optionally
4 | persists it on the file system when the server shuts down. It is meant for
5 | development purpose only.
6 |
7 | # Options
8 |
9 | The following [options](README.md#options) are available.
10 |
11 | ```yml
12 | databases:
13 | memory:
14 | data:
15 | $ref: my_data_file.yml
16 | save: true
17 | ```
18 |
19 | The `data` option is an object containing all the collections loaded on server
20 | startup. It defaults to an empty database. For example:
21 |
22 | ```yml
23 | users:
24 | - id: '1'
25 | name: Anthony
26 | - id: '2'
27 | name: David
28 | managers: []
29 | ```
30 |
31 | If the `save` option is `true`, the data will be saved back to the file when the
32 | server shuts down, providing the `data` option used a
33 | [reference](../configuration/references.md).
34 |
--------------------------------------------------------------------------------
/src/middleware/action/current_data/main.js:
--------------------------------------------------------------------------------
1 | import { parallelResolve } from './parallel.js'
2 | import { serialResolve } from './serial.js'
3 |
4 | // Add `action.currentData`, i.e. current models for the write actions about
5 | // to be fired.
6 | // Also adds `action.dataPaths` for `patch` and `delete` commands.
7 | export const addCurrentData = ({ top: { command }, ...rest }, nextLayer) => {
8 | const resolver = resolvers[command.type]
9 |
10 | if (resolver === undefined) {
11 | return
12 | }
13 |
14 | return resolver(rest, nextLayer)
15 | }
16 |
17 | // `find` command does not need `currentData`
18 | // `patch` and `delete` use `args.filter|id` so need to be run serially,
19 | // waiting for their parent.
20 | // But `create` and `upsert` can be run in parallel.
21 | const resolvers = {
22 | create: parallelResolve,
23 | upsert: parallelResolve,
24 | patch: serialResolve,
25 | delete: serialResolve,
26 | }
27 |
--------------------------------------------------------------------------------
/src/filter/operators/lt_gt_lte_gte.js:
--------------------------------------------------------------------------------
1 | import { parseAsIs, validateSameType } from './common.js'
2 |
3 | // `{ attribute: { _lt: value } }`
4 | const evalLt = ({ attr, value }) => attr < value
5 |
6 | // `{ attribute: { _gt: value } }`
7 | const evalGt = ({ attr, value }) => attr > value
8 |
9 | // `{ attribute: { _lte: value } }`
10 | const evalLte = ({ attr, value }) => attr <= value
11 |
12 | // `{ attribute: { _gte: value } }`
13 | const evalGte = ({ attr, value }) => attr >= value
14 |
15 | export const lt = {
16 | parse: parseAsIs,
17 | validate: validateSameType,
18 | eval: evalLt,
19 | }
20 | export const gt = {
21 | parse: parseAsIs,
22 | validate: validateSameType,
23 | eval: evalGt,
24 | }
25 | export const lte = {
26 | parse: parseAsIs,
27 | validate: validateSameType,
28 | eval: evalLte,
29 | }
30 | export const gte = {
31 | parse: parseAsIs,
32 | validate: validateSameType,
33 | eval: evalGte,
34 | }
35 |
--------------------------------------------------------------------------------
/docs/server/configuration/formats.md:
--------------------------------------------------------------------------------
1 | # Formats
2 |
3 | The same formats are supported for the
4 | [configuration file](configuration.md#configuration-file) as for the
5 | [client request payloads and the server responses](../../client/protocols/formats.md),
6 | except:
7 |
8 | - the following ones are also available: [JavaScript](#javascript).
9 | - the [raw](../../client/protocols/formats.md#raw) format is the default one.
10 | This means configuration files with unrecognized file extensions will be
11 | loaded as strings.
12 |
13 | Most of the examples in this documentation use
14 | [YAML](../../client/protocols/formats.md#yaml) for the
15 | [configuration properties](configuration.md#properties).
16 |
17 | # JavaScript
18 |
19 | ```js
20 | // Comment
21 |
22 | export default {
23 | limits: {
24 | pagesize: 10,
25 | },
26 | protocols: {
27 | http: {
28 | hostname: 'myhostname',
29 | },
30 | },
31 | }
32 | ```
33 |
--------------------------------------------------------------------------------
/src/functions/run.js:
--------------------------------------------------------------------------------
1 | import { addGenPbHandler } from '../errors/handler.js'
2 |
3 | import { getParams } from './params/values.js'
4 | import { stringifyConfigFunc } from './tokenize.js'
5 |
6 | // Process config function, i.e. fires it and returns its value
7 | const eRunConfigFunc = ({
8 | configFunc,
9 | mInput,
10 | mInput: { serverParams },
11 | params,
12 | }) => {
13 | // If this is not config function, returns as is
14 | if (typeof configFunc !== 'function') {
15 | return configFunc
16 | }
17 |
18 | const paramsA = getParams(mInput, { params, serverParams, mutable: false })
19 |
20 | return configFunc(paramsA)
21 | }
22 |
23 | export const runConfigFunc = addGenPbHandler(eRunConfigFunc, {
24 | message: ({ configFunc }) =>
25 | `Function failed: '${stringifyConfigFunc({ configFunc })}'`,
26 | reason: 'CONFIG_RUNTIME',
27 | extra: ({ configFunc }) => ({ value: stringifyConfigFunc({ configFunc }) }),
28 | })
29 |
--------------------------------------------------------------------------------
/src/patch/validate/types/empty.js:
--------------------------------------------------------------------------------
1 | // Since we do not check for `empty` against `patchOp.argument` before
2 | // model.ATTR resolution, we do it now
3 | export const checkEmpty = ({ opVal, operator: { argument }, type }) => {
4 | if (argument === undefined) {
5 | return
6 | }
7 |
8 | if (hasWrongNull({ opVal, argument })) {
9 | return `the argument is invalid. Patch operator '${type}' argument must be not be empty`
10 | }
11 |
12 | if (hasWrongNulls({ opVal, argument })) {
13 | return `the argument is invalid. Patch operator '${type}' argument must be not contain empty items`
14 | }
15 | }
16 |
17 | const hasWrongNull = ({ opVal, argument }) =>
18 | (opVal === undefined || opVal === null) && !argument.includes('empty')
19 |
20 | const hasWrongNulls = ({ opVal, argument }) =>
21 | Array.isArray(opVal) &&
22 | // eslint-disable-next-line unicorn/no-null
23 | opVal.includes(null) &&
24 | !argument.includes('empty[]')
25 |
--------------------------------------------------------------------------------
/docs/server/usage/README.md:
--------------------------------------------------------------------------------
1 | # Installation
2 |
3 | ```shell
4 | $ npm install autoserver
5 | ```
6 |
7 | # Usage
8 |
9 | From the command line:
10 |
11 | ```shell
12 | $ autoserver [INSTRUCTION] [OPTIONS]
13 | ```
14 |
15 | The following instructions are available:
16 |
17 | - [run](run.md) (default): start the server
18 |
19 | `OPTIONS` are dot-separated flags specific to each instruction.
20 |
21 | ```shell
22 | $ autoserver run --protocols.http.port=5001
23 | ```
24 |
25 | # Node.js
26 |
27 | The server can also be used from Node.js:
28 |
29 | ```js
30 | import { run } from 'autoserver'
31 |
32 | run({ protocols: { http: { port: 5001 } } })
33 | ```
34 |
35 | Here we used the `INSTRUCTION` `run`, but any `INSTRUCTION` can be used.
36 |
37 | Options are directly passed as an object argument.
38 |
39 | Every instruction returns a promise. If an error occurs, that promise is
40 | rejected with an [exception object](error.md#exceptions).
41 |
--------------------------------------------------------------------------------
/src/run/exit/setup.js:
--------------------------------------------------------------------------------
1 | import process from 'node:process'
2 |
3 | import { signals } from 'signal-exit'
4 |
5 | import { gracefulExit } from './graceful_exit.js'
6 |
7 | // Make sure the server stops when graceful exits are possible
8 | // Also send related events
9 | // We cannot handle `process.exit()` since graceful exit is async
10 | export const setupGracefulExit = ({
11 | protocolAdapters,
12 | dbAdapters,
13 | stopProcessErrors,
14 | config,
15 | }) => {
16 | const exitFunc = gracefulExit.bind(undefined, {
17 | protocolAdapters,
18 | dbAdapters,
19 | stopProcessErrors,
20 | config,
21 | })
22 |
23 | const exitSignals = getExitSignals()
24 | exitSignals.forEach((exitSignal) => {
25 | process.on(exitSignal, exitFunc)
26 | })
27 |
28 | return { exitFunc }
29 | }
30 |
31 | // Add `SIGUSR2` for Nodemon
32 | const getExitSignals = () =>
33 | signals.includes('SIGUSR2') ? signals : [...signals, 'SIGUSR2']
34 |
--------------------------------------------------------------------------------
/src/middleware/final/send_response/serialize.js:
--------------------------------------------------------------------------------
1 | import { Buffer } from 'node:buffer'
2 |
3 | import { isType } from '../../../content_types.js'
4 |
5 | // Transform content to a buffer
6 | export const serializeContent = async ({
7 | format,
8 | content,
9 | type,
10 | topargs,
11 | error,
12 | }) => {
13 | const contentA = await stringifyContent({ format, content, type })
14 |
15 | const contentB = applySilent({ content: contentA, topargs, error })
16 |
17 | const contentC = Buffer.from(contentB)
18 |
19 | return contentC
20 | }
21 |
22 | const stringifyContent = async ({ format, content, type }) =>
23 | isType(type, 'object') ? await format.serializeContent(content) : content
24 |
25 | // When `args.silent` is used (unless this is an error response).
26 | const applySilent = ({ content, topargs: { silent } = {}, error }) => {
27 | if (silent && error === undefined) {
28 | return ''
29 | }
30 |
31 | return content
32 | }
33 |
--------------------------------------------------------------------------------
/src/patch/validate/types/available.js:
--------------------------------------------------------------------------------
1 | // Available types in `patchOp.attribute|argument`
2 | export const TYPES = {
3 | empty: {
4 | test: (val) => val === undefined || val === null,
5 | name: 'null',
6 | pluralname: 'null',
7 | },
8 |
9 | string: {
10 | test: (val) => typeof val === 'string',
11 | name: 'a string',
12 | pluralname: 'strings',
13 | },
14 |
15 | boolean: {
16 | test: (val) => typeof val === 'boolean',
17 | name: 'true or false',
18 | pluralname: 'true or false',
19 | },
20 |
21 | integer: {
22 | test: (val) => Number.isInteger(val),
23 | name: 'an integer',
24 | pluralname: 'integers',
25 | },
26 |
27 | number: {
28 | test: (val) => Number.isFinite(val),
29 | name: 'a number',
30 | pluralname: 'numbers',
31 | },
32 |
33 | object: {
34 | test: (val) => typeof val === 'object',
35 | name: 'an object',
36 | pluralname: 'objects',
37 | },
38 | }
39 |
--------------------------------------------------------------------------------
/src/protocols/request/origin.js:
--------------------------------------------------------------------------------
1 | import { throwPb } from '../../errors/props.js'
2 | import { getLimits } from '../../limits.js'
3 |
4 | import { validateString } from './validate.js'
5 |
6 | export const parseOrigin = ({
7 | protocolAdapter,
8 | protocolAdapter: { getUrl, getOrigin },
9 | specific,
10 | config,
11 | }) => {
12 | // Only used to validate URL length
13 | const url = getUrl({ specific })
14 |
15 | validateString(url, 'url', protocolAdapter)
16 | validateUrl({ url, config })
17 |
18 | const origin = getOrigin({ specific })
19 |
20 | validateString(origin, 'origin', protocolAdapter)
21 |
22 | return { origin }
23 | }
24 |
25 | const validateUrl = ({ url, config }) => {
26 | const { maxUrlLength } = getLimits({ config })
27 |
28 | if (url.length <= maxUrlLength) {
29 | return
30 | }
31 |
32 | throwPb({
33 | reason: 'URL_LIMIT',
34 | extra: { value: url.length, limit: maxUrlLength },
35 | })
36 | }
37 |
--------------------------------------------------------------------------------
/src/rpc/adapters/graphql/load/types/object/final_fields/default.js:
--------------------------------------------------------------------------------
1 | export const getDefaultValue = (def, opts) => {
2 | const shouldSetDefault = defaultValueTests.every((func) => func(def, opts))
3 |
4 | if (!shouldSetDefault) {
5 | return
6 | }
7 |
8 | return def.default
9 | }
10 |
11 | const hasDefaultValue = (def) =>
12 | def.default !== undefined && def.default !== null
13 |
14 | // Only for `args.data`
15 | const isDataArgument = (def, { inputObjectType }) => inputObjectType === 'data'
16 |
17 | // Only applied when model is created, e.g. on `create` or `upsert`
18 | const isNotPatchData = ({ command }) => DEFAULT_COMMANDS.has(command)
19 |
20 | const DEFAULT_COMMANDS = new Set(['create', 'upsert'])
21 |
22 | // Config function are skipped
23 | const isStatic = (def) => typeof def.default !== 'function'
24 |
25 | const defaultValueTests = [
26 | hasDefaultValue,
27 | isDataArgument,
28 | isNotPatchData,
29 | isStatic,
30 | ]
31 |
--------------------------------------------------------------------------------
/docs/client/arguments/README.md:
--------------------------------------------------------------------------------
1 | # Arguments
2 |
3 | Each [CRUD command](../request/crud.md) has its own set of
4 | [arguments](../rpc/README.md).
5 |
6 | The following [arguments](../rpc/README.md#rpc) are available:
7 |
8 | - [`id`](filtering.md#id-argument)
9 | - [`filter`](filtering.md)
10 | - [`data`](../request/crud.md#create-command)
11 | - [`order`](sorting.md)
12 | - [`populate`](../request/relations.md#populating-nested-collections) and
13 | [`cascade`](../request/relations.md#deleting-nested-collections)
14 | - pagination: [`pagesize`](pagination.md#page-size),
15 | [`page`](pagination.md#offset-pagination),
16 | [`after`](pagination.md#cursor-pagination) and
17 | [`before`](pagination.md#backward-iteration)
18 | - [`select`](selecting.md)
19 | - [`rename`](renaming.md)
20 | - [`silent`](silent.md)
21 | - [`dryrun`](dryrun.md)
22 | - [`params`](params.md)
23 |
24 | The following query variable is also available: [`compress`](compression.md).
25 |
--------------------------------------------------------------------------------
/src/middleware/sequencer/read/paginate/attr.js:
--------------------------------------------------------------------------------
1 | // Retrieve list of nested attributes as `{ attrName, weight }`
2 | // `weight` is the number of models spawn by each instance of this child action
3 | export const getNestedAttrs = ({ childActions }) => {
4 | const topChildActions = childActions.filter(
5 | ({ parentAction: { commandpath } }) => commandpath.length === 1,
6 | )
7 | const nestedAttrs = topChildActions.map(getNestedAttr)
8 | return nestedAttrs
9 | }
10 |
11 | const getNestedAttr = ({
12 | parentAction: {
13 | commandpath: [attrName],
14 | },
15 | childActions,
16 | }) => {
17 | const weight = getWeight({ childActions })
18 | return { attrName, weight }
19 | }
20 |
21 | const getWeight = ({ childActions }) => {
22 | const childWeights = childActions
23 | .map((childAction) => getWeight({ childActions: childAction.childActions }))
24 | .reduce((sum, weightA) => sum + weightA, 0)
25 | return childWeights + 1
26 | }
27 |
--------------------------------------------------------------------------------
/src/utils/functional/group.js:
--------------------------------------------------------------------------------
1 | // Group array of objects together according to a specific key
2 | // `key` can a string (the object key), an array (several object keys) or
3 | // a function returning a string.
4 | export const groupBy = (array, key) =>
5 | array.reduce(groupByReducer.bind(undefined, key), {})
6 |
7 | const groupByReducer = (key, groups, obj) => {
8 | const groupName = getGroupName(key, obj)
9 | const { [groupName]: currentGroup = [] } = groups
10 | const newGroup = [...currentGroup, obj]
11 | return { ...groups, [groupName]: newGroup }
12 | }
13 |
14 | const getGroupName = (key, obj) => {
15 | if (typeof key === 'function') {
16 | return key(obj)
17 | }
18 |
19 | if (Array.isArray(key)) {
20 | return key.map((name) => obj[name]).join(',')
21 | }
22 |
23 | return obj[key]
24 | }
25 |
26 | export const groupValuesBy = (array, key) => {
27 | const groups = groupBy(array, key)
28 | return Object.values(groups)
29 | }
30 |
--------------------------------------------------------------------------------
/src/utils/functional/reduce.js:
--------------------------------------------------------------------------------
1 | import { promiseThen } from './promise.js'
2 |
3 | // Like Array.reduce(), but supports async
4 | // eslint-disable-next-line max-params
5 | export const reduceAsync = (array, mapFunc, prevVal, secondMapFunc) =>
6 | asyncReducer(prevVal, { array, mapFunc, secondMapFunc })
7 |
8 | const asyncReducer = (prevVal, input) => {
9 | const { array, mapFunc, index = 0 } = input
10 |
11 | if (index === array.length) {
12 | return prevVal
13 | }
14 |
15 | const nextVal = mapFunc(prevVal, array[index], index, array)
16 | const inputA = { ...input, index: index + 1 }
17 |
18 | return promiseThen(nextVal, applySecondMap.bind(undefined, prevVal, inputA))
19 | }
20 |
21 | const applySecondMap = (prevVal, input, nextVal) => {
22 | if (!input.secondMapFunc) {
23 | return asyncReducer(nextVal, input)
24 | }
25 |
26 | const nextValA = input.secondMapFunc(prevVal, nextVal)
27 | return asyncReducer(nextValA, input)
28 | }
29 |
--------------------------------------------------------------------------------
/src/adapters/get.js:
--------------------------------------------------------------------------------
1 | // Retrieve an adapter by its name
2 | export const getAdapter = ({ adapters, key, name }) => {
3 | const adapter = adapters[key]
4 |
5 | if (adapter !== undefined) {
6 | return adapter.wrapped
7 | }
8 |
9 | throw new Error(`Unsupported ${name}: '${key}'`)
10 | }
11 |
12 | // Retrieve all adapters' names
13 | export const getNames = (adapters) => adapters.map(({ name }) => name)
14 |
15 | // Retrieve all fields of adapters, for a given field
16 | export const getMember = (adapters, member, defaultValue) => {
17 | const members = adapters.map((adapter) =>
18 | getAdapterMember({ adapter, member, defaultValue }),
19 | )
20 | const membersA = Object.assign({}, ...members)
21 | return membersA
22 | }
23 |
24 | const getAdapterMember = ({ adapter, member, defaultValue }) => {
25 | const memberA = adapter[member]
26 | const memberB = memberA === undefined ? defaultValue : memberA
27 | return { [adapter.name]: memberB }
28 | }
29 |
--------------------------------------------------------------------------------
/docs/server/databases/README.md:
--------------------------------------------------------------------------------
1 | # Databases
2 |
3 | Databases are specified with the `collection.database`
4 | [configuration property](../configuration/configuration.md#properties).
5 |
6 | ```yml
7 | collections:
8 | example_collection:
9 | database: mongodb
10 | ```
11 |
12 | This means multiple databases can be mixed on the same API, and collections can
13 | either share the same databases or use different ones.
14 |
15 | # Options
16 |
17 | Databases options are specified with the `databases`
18 | [configuration property](../configuration/configuration.md#properties).
19 |
20 | ```yml
21 | databases:
22 | memory:
23 | save: false
24 | ```
25 |
26 | # Available databases
27 |
28 | The available databases are:
29 |
30 | - [`memory`](memorydb.md): an in-memory database, for development purpose.
31 | - [`mongodb`](mongodb.md)
32 |
33 | The default database is `memory`. To change it, use a
34 | [`default` collection](../data_model/collections.md#default-collection).
35 |
--------------------------------------------------------------------------------
/src/config/reducers/colls_default.js:
--------------------------------------------------------------------------------
1 | import omit from 'omit.js'
2 |
3 | import { mapValues } from '../../utils/functional/map.js'
4 | import { deepMerge } from '../../utils/functional/merge.js'
5 |
6 | // Applies `config.collections.default` to each collection
7 | export const applyCollsDefault = ({
8 | config: { collections = {}, collections: { default: collDefault } = {} },
9 | }) => {
10 | const collectionsA = omit.default(collections, ['default'])
11 | const collectionsB = mapValues(collectionsA, (coll) =>
12 | applyCollDefault({ coll, collDefault }),
13 | )
14 |
15 | return { collections: collectionsB }
16 | }
17 |
18 | const applyCollDefault = ({ coll, collDefault }) => {
19 | const shouldApply = isProperColl(collDefault) && isProperColl(coll)
20 |
21 | if (!shouldApply) {
22 | return coll
23 | }
24 |
25 | return deepMerge(collDefault, coll)
26 | }
27 |
28 | const isProperColl = (coll) =>
29 | coll !== null && coll !== undefined && typeof coll === 'object'
30 |
--------------------------------------------------------------------------------
/src/protocols/request/content_negotiation/format.js:
--------------------------------------------------------------------------------
1 | import { addGenPbHandler } from '../../../errors/handler.js'
2 | import { DEFAULT_FORMAT, getFormat, getMimes } from '../../../formats/get.js'
3 |
4 | // Retrieve format asked by client for the response payload
5 | export const getFormatFunc = ({ queryvars, format }) => {
6 | const formatA = getFormatName({ queryvars, format })
7 |
8 | if (formatA === undefined) {
9 | return
10 | }
11 |
12 | const formatB = eGetFormat(formatA, { safe: true })
13 | return formatB
14 | }
15 |
16 | const getFormatName = ({ queryvars, format }) =>
17 | queryvars.format ||
18 | // E.g. MIME in Content-Type HTTP header
19 | format ||
20 | DEFAULT_FORMAT.name
21 |
22 | const getExtra = (format) => {
23 | const suggestions = getMimes({ safe: true })
24 | return { kind: 'format', value: [format], suggestions }
25 | }
26 |
27 | const eGetFormat = addGenPbHandler(getFormat, {
28 | reason: 'RESPONSE_NEGOTIATION',
29 | extra: getExtra,
30 | })
31 |
--------------------------------------------------------------------------------
/src/databases/adapters/mongodb/opts.js:
--------------------------------------------------------------------------------
1 | export const opts = {
2 | type: 'object',
3 | additionalProperties: false,
4 | properties: {
5 | hostname: {
6 | oneOf: [
7 | {
8 | type: 'string',
9 | },
10 | {
11 | type: 'array',
12 | items: {
13 | type: 'string',
14 | },
15 | minItems: 1,
16 | },
17 | ],
18 | },
19 | port: {
20 | oneOf: [
21 | {
22 | type: 'integer',
23 | minimum: 0,
24 | maximum: 65_535,
25 | },
26 | {
27 | type: 'array',
28 | items: {
29 | type: 'integer',
30 | },
31 | minItems: 1,
32 | },
33 | ],
34 | },
35 | username: {
36 | type: 'string',
37 | },
38 | password: {
39 | type: 'string',
40 | },
41 | dbname: {
42 | type: 'string',
43 | },
44 | opts: {
45 | type: 'object',
46 | },
47 | },
48 | }
49 |
--------------------------------------------------------------------------------
/src/json_refs/load.js:
--------------------------------------------------------------------------------
1 | import { stat } from 'node:fs/promises'
2 |
3 | import { addErrorHandler, addGenErrorHandler } from '../errors/handler.js'
4 | import { DEFAULT_RAW_FORMAT, getByExt } from '../formats/get.js'
5 |
6 | // Load the file pointing to by the JSON reference
7 | export const load = async ({ path }) => {
8 | // Checks that the file exists
9 | await eStat(path)
10 |
11 | const format = eGetByExt({ path })
12 | const content = await eLoadFile({ format, path })
13 |
14 | return content
15 | }
16 |
17 | const eStat = addGenErrorHandler(stat, {
18 | message: (path) => `Config file does not exist: '${path}'`,
19 | reason: 'CONFIG_VALIDATION',
20 | })
21 |
22 | const eGetByExt = addErrorHandler(getByExt, () => DEFAULT_RAW_FORMAT)
23 |
24 | const loadFile = ({ format, path }) => format.parseFile(path, { compat: false })
25 |
26 | const eLoadFile = addGenErrorHandler(loadFile, {
27 | message: ({ path }) => `Config file could not be parsed: '${path}'`,
28 | reason: 'CONFIG_VALIDATION',
29 | })
30 |
--------------------------------------------------------------------------------
/src/utils/sums.js:
--------------------------------------------------------------------------------
1 | // Some parameters are filtered out in logs and in error responses
2 | // because they can get too big, e.g. `args.data`, `response.data` and `payload`
3 | // `sumParams` summarize them by their size and length, e.g. `payloadsize` and
4 | // `payloadcount`
5 | export const getSumParams = ({ attrName, value }) => {
6 | if (value === undefined) {
7 | return
8 | }
9 |
10 | const size = getSize({ attrName, value })
11 | const count = getCount({ attrName, value })
12 | return { ...size, ...count }
13 | }
14 |
15 | const getSize = ({ attrName, value }) => {
16 | try {
17 | const size = JSON.stringify(value).length
18 | const name = `${attrName}size`
19 | return { [name]: size }
20 | // Returns `size` `undefined` if not JSON
21 | } catch {}
22 | }
23 |
24 | const getCount = ({ attrName, value }) => {
25 | if (!Array.isArray(value)) {
26 | return
27 | }
28 |
29 | const count = value.length
30 | const name = `${attrName}count`
31 | return { [name]: count }
32 | }
33 |
--------------------------------------------------------------------------------
/src/databases/features/filter.js:
--------------------------------------------------------------------------------
1 | import { difference } from '../../utils/functional/difference.js'
2 | import { getWordsList } from '../../utils/string.js'
3 |
4 | // Adapter feature 'filter:_OPERATOR' allows for
5 | // `args.filter: { attrName: { _OPERATOR: value } }`
6 | export const filterValidator = ({ features, filterFeatures }) => {
7 | const ops = getOps({ features, filterFeatures })
8 |
9 | if (ops.length === 0) {
10 | return
11 | }
12 |
13 | if (ops.includes('_or')) {
14 | return "In 'filter' argument, must not use an array of alternatives"
15 | }
16 |
17 | if (ops.includes('sibling')) {
18 | return "In 'filter' argument, must not use values prefixed with 'model.'"
19 | }
20 |
21 | const opsA = getWordsList(ops, { op: 'nor', quotes: true })
22 | return `In 'filter' argument, must not use the operators ${opsA}`
23 | }
24 |
25 | const getOps = ({ features, filterFeatures }) =>
26 | difference(filterFeatures, features).map((feature) =>
27 | feature.replace(/.*:/u, ''),
28 | )
29 |
--------------------------------------------------------------------------------
/src/middleware/final/send_response/compress.js:
--------------------------------------------------------------------------------
1 | import { DEFAULT_ALGO } from '../../../compress/get.js'
2 | import { shouldCompress } from '../../../compress/info.js'
3 |
4 | import { TYPES } from './types.js'
5 |
6 | // Response body compression
7 | export const compressContent = async ({
8 | content,
9 | compressResponse,
10 | type,
11 | contentType,
12 | }) => {
13 | const algo = getAlgo({ compressResponse, type, contentType })
14 |
15 | const contentA = await algo.compress(content)
16 |
17 | return { content: contentA, compressResponse: algo }
18 | }
19 |
20 | const getAlgo = ({ compressResponse, type, contentType }) => {
21 | const isInvalid =
22 | compressResponse === undefined ||
23 | typeof compressResponse === 'string' ||
24 | !willCompress({ type, contentType })
25 |
26 | if (isInvalid) {
27 | return DEFAULT_ALGO
28 | }
29 |
30 | return compressResponse
31 | }
32 |
33 | const willCompress = ({ type, contentType }) =>
34 | TYPES[type].isText || shouldCompress({ contentType })
35 |
--------------------------------------------------------------------------------
/src/middleware/database/rename_ids/filter.js:
--------------------------------------------------------------------------------
1 | import { mapNodes } from '../../../filter/crawl.js'
2 | import { isSiblingValue } from '../../../filter/siblings.js'
3 |
4 | // Modify `args.filter`
5 | export const renameFilter = ({ value, newIdName, oldIdName }) =>
6 | mapNodes(value, (node) => renameFilterId({ node, newIdName, oldIdName }))
7 |
8 | const renameFilterId = ({ node, newIdName, oldIdName }) => {
9 | const nodeA = renameFilterSiblings({ node, newIdName, oldIdName })
10 |
11 | if (nodeA.attrName !== oldIdName) {
12 | return nodeA
13 | }
14 |
15 | return { ...nodeA, attrName: newIdName }
16 | }
17 |
18 | const renameFilterSiblings = ({
19 | node,
20 | node: { value },
21 | newIdName,
22 | oldIdName,
23 | }) => {
24 | if (!isSiblingValue({ value })) {
25 | return node
26 | }
27 |
28 | const { value: attrName } = value
29 |
30 | if (attrName !== oldIdName) {
31 | return node
32 | }
33 |
34 | const nodeA = { ...node, value: { ...value, value: newIdName } }
35 | return nodeA
36 | }
37 |
--------------------------------------------------------------------------------
/src/functions/tokenize.js:
--------------------------------------------------------------------------------
1 | // Break down ' \\( inlineFunc ) '
2 | // into tokens: ' ', '\', '(', ' inlineFunc ', ')', ' '
3 | export const tokenizeInlineFunc = (inlineFunc) =>
4 | INLINE_FUNC_REGEXP.exec(inlineFunc)
5 |
6 | const INLINE_FUNC_REGEXP = /^(\s*)(?