├── 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 | {"id":"7","name":"JSON","photo_urls":["urlA"],"friends":[]} -------------------------------------------------------------------------------- /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 | GraphQL schema 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
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*)(?\\?)(\()(?.*)(\))(\s*)$/su 7 | 8 | // Remove outer parenthesis from inline function 9 | export const getInlineFunc = (inlineFunc) => { 10 | const parts = tokenizeInlineFunc(inlineFunc) 11 | return parts.groups.body 12 | } 13 | 14 | // Retrieves inline config function body 15 | export const stringifyConfigFunc = ({ configFunc, configFunc: { name } }) => { 16 | if (name && name !== 'anonymous') { 17 | return `${name}()` 18 | } 19 | 20 | const funcStr = configFunc.toString() 21 | const parts = BODY_REGEXP.exec(funcStr) 22 | 23 | if (parts === null) { 24 | return funcStr 25 | } 26 | 27 | return parts[1] 28 | } 29 | 30 | // Extracts inline function. Only works on inline functions. 31 | const BODY_REGEXP = /^function anonymous\(\{.*return \((.*)\)/su 32 | -------------------------------------------------------------------------------- /src/middleware/request_response/data_validation.js: -------------------------------------------------------------------------------- 1 | import { addGenErrorHandler } from '../../errors/handler.js' 2 | import { validate } from '../../validation/validate.js' 3 | 4 | // Custom data validation middleware 5 | // Check that newData passes config validation 6 | // E.g. if a model is marked as `required` or `minimum: 10` in the 7 | // config, this will be validated here 8 | export const dataValidation = ({ 9 | args: { newData, currentData }, 10 | collname, 11 | config: { 12 | shortcuts: { validateMap }, 13 | }, 14 | mInput, 15 | }) => { 16 | if (newData === undefined) { 17 | return 18 | } 19 | 20 | const compiledJsonSchema = validateMap[collname] 21 | 22 | newData.forEach((data, index) => { 23 | eValidate({ 24 | compiledJsonSchema, 25 | data, 26 | extra: { mInput, currentDatum: currentData[index] }, 27 | }) 28 | }) 29 | } 30 | 31 | const eValidate = addGenErrorHandler(validate, { 32 | reason: 'VALIDATION', 33 | message: (input, { message }) => `Wrong parameters: ${message}`, 34 | }) 35 | -------------------------------------------------------------------------------- /src/rpc/adapters/graphql/load/types/object/nested_colls.js: -------------------------------------------------------------------------------- 1 | import omit from 'omit.js' 2 | 3 | // Create nested collections definitions 4 | export const getNestedColl = (def, { inputObjectType, topDef }) => { 5 | const { target, isArray } = def 6 | 7 | // Only for nested collections, that are not filter arguments 8 | const isNested = target !== undefined && inputObjectType !== 'filter' 9 | 10 | if (!isNested) { 11 | return def 12 | } 13 | 14 | const topLevelModel = Object.values(topDef.attributes).find((topDefA) => 15 | topLevelModelMatches(def, topDefA), 16 | ) 17 | // Command description is only used for Query|Mutation children, 18 | // not for recursive attributes, which use the normal `attr.description` 19 | const topLevelModelA = omit.default(topLevelModel, ['commandDescription']) 20 | const topLevelModelB = { ...topLevelModelA, isArray } 21 | 22 | return topLevelModelB 23 | } 24 | 25 | const topLevelModelMatches = ({ target, command }, topDef) => 26 | topDef.collname === target && topDef.command === command 27 | -------------------------------------------------------------------------------- /src/databases/adapters/mongodb/query/find/main.js: -------------------------------------------------------------------------------- 1 | import { limitResponse } from './limit.js' 2 | import { offsetResponse } from './offset.js' 3 | import { getQueryFilter } from './operators.js' 4 | import { sortResponse } from './order.js' 5 | 6 | // Find models 7 | export const find = (input) => { 8 | const { filterIds } = input 9 | const func = filterIds && filterIds.length === 1 ? findOne : findMany 10 | return func(input) 11 | } 12 | 13 | const findOne = async ({ collection, filterIds }) => { 14 | const model = await collection.findOne({ _id: filterIds[0] }) 15 | return model === undefined || model === null ? [] : [model] 16 | } 17 | 18 | const findMany = ({ collection, filter, offset, limit, order }) => { 19 | const queryFilter = getQueryFilter(filter) 20 | const cursor = collection.find(queryFilter) 21 | 22 | const cursorA = limitResponse({ cursor, limit }) 23 | const cursorB = offsetResponse({ cursor: cursorA, offset }) 24 | const cursorC = sortResponse({ cursor: cursorB, order }) 25 | 26 | return cursorC.toArray() 27 | } 28 | -------------------------------------------------------------------------------- /src/log/params.js: -------------------------------------------------------------------------------- 1 | import { reduceParams } from '../functions/params/reduce.js' 2 | import { getParams } from '../functions/params/values.js' 3 | 4 | // Get log-specific config parameters 5 | export const getLogParams = ({ 6 | params, 7 | config, 8 | mInput = { config }, 9 | event, 10 | phase, 11 | level, 12 | message, 13 | }) => { 14 | const levelA = getLevel({ level, event }) 15 | 16 | const paramsA = { ...params, event, phase, level: levelA, message } 17 | const paramsB = getParams(mInput, { params: paramsA }) 18 | const log = reduceParams({ params: paramsB }) 19 | 20 | // Used with `runConfigFunc()` by log providers 21 | const configFuncInput = { params: { ...paramsA, log }, mInput } 22 | 23 | return { log, configFuncInput } 24 | } 25 | 26 | // Level defaults to `error` for event `failure`, and to `log` for other events 27 | const getLevel = ({ level, event }) => { 28 | if (level) { 29 | return level 30 | } 31 | 32 | if (event === 'failure') { 33 | return 'error' 34 | } 35 | 36 | return 'log' 37 | } 38 | -------------------------------------------------------------------------------- /src/protocols/adapters/http/main.js: -------------------------------------------------------------------------------- 1 | import { defaults } from './defaults.js' 2 | import { getHeaders } from './headers.js' 3 | import { getInput } from './input.js' 4 | import { getIp } from './ip.js' 5 | import { getMethod } from './method.js' 6 | import { opts } from './opts.js' 7 | import { getOrigin, getUrl } from './origin.js' 8 | import { getPath } from './path.js' 9 | import { getPayload, hasPayload } from './payload/main.js' 10 | import { getQueryString } from './query_string.js' 11 | import { send } from './send/main.js' 12 | import { startServer } from './start.js' 13 | // eslint-disable-next-line import/max-dependencies 14 | import { stopServer } from './stop.js' 15 | 16 | export const http = { 17 | name: 'http', 18 | title: 'HTTP', 19 | description: "HTTP server's options", 20 | startServer, 21 | stopServer, 22 | getUrl, 23 | getOrigin, 24 | getQueryString, 25 | getHeaders, 26 | getMethod, 27 | getPath, 28 | getPayload, 29 | hasPayload, 30 | send, 31 | getIp, 32 | getInput, 33 | opts, 34 | defaults, 35 | } 36 | -------------------------------------------------------------------------------- /docs/server/quality/limits.md: -------------------------------------------------------------------------------- 1 | # Options 2 | 3 | The following limits can be configured with the `limits` 4 | [configuration property](../configuration/configuration.md#properties): 5 | 6 | - `limits.maxpayload` `{integer|string}` (defaults to `10MB`): Max size of 7 | request payloads, in bytes. Also used as the max URL length. Can use `KB`, 8 | `MB`, `GB` or `TB`. 9 | - `limits.pagesize` `{integer}` (defaults to `100`): see 10 | [pagination](../../client/arguments/pagination.md) 11 | - `limits.maxmodels` `{integer}` (defaults to `100 * pagesize`, i.e. `10000`): 12 | see [pagination](../../client/arguments/pagination.md) 13 | 14 | ```yml 15 | limits: 16 | maxpayload: 5MB 17 | pagesize: 200 18 | maxmodels: 10000 19 | ``` 20 | 21 | # System limits 22 | 23 | The following limits cannot be configured: 24 | 25 | - maximum number of nested commands: `50` 26 | - maximum length of attributes' value: `2KB` 27 | - maximum number of attributes per model: `50` 28 | - maximum length of model's or attribute's name: `100` characters 29 | - request timeout: `5` seconds 30 | -------------------------------------------------------------------------------- /src/formats/adapters/ini.js: -------------------------------------------------------------------------------- 1 | import { parse as iniParse, stringify as iniStringify } from 'ini' 2 | 3 | import { fullRecurseMap } from '../../utils/functional/map.js' 4 | 5 | // Parses an INI file 6 | const parse = ({ content }) => iniParse(content) 7 | 8 | // Serializes an INI file 9 | const serialize = ({ content }) => { 10 | const contentA = fullRecurseMap(content, escapeEmptyArrays) 11 | return iniStringify(contentA) 12 | } 13 | 14 | // Empty arrays are ignored by `node-ini`, so we need to escape them 15 | const escapeEmptyArrays = (val) => { 16 | const isEmptyArray = Array.isArray(val) && val.length === 0 17 | 18 | if (!isEmptyArray) { 19 | return val 20 | } 21 | 22 | return '[]' 23 | } 24 | 25 | export const ini = { 26 | name: 'ini', 27 | title: 'INI', 28 | extensions: ['ini', 'in', 'cfg', 'conf'], 29 | mimeExtensions: ['+ini'], 30 | // `node-ini` only supports UTF-8 31 | // eslint-disable-next-line unicorn/text-encoding-identifier-case 32 | charsets: ['utf-8'], 33 | jsonCompat: ['subset'], 34 | parse, 35 | serialize, 36 | } 37 | --------------------------------------------------------------------------------