local Errors = require "kong.db.errors" local uuid = require "kong.tools.uuid" local arguments = require "kong.api.arguments" local workspaces = require "kong.workspaces" local app_helpers = require "lapis.application" local kong = kong local escape_uri = ngx.escape_uri local unescape_uri = ngx.unescape_uri local tonumber = tonumber local tostring = tostring local select = select local pairs = pairs local null = ngx.null local type = type local fmt = string.format local concat = table.concat local re_match = ngx.re.match local splitn = require("kong.tools.string").splitn local get_default_exit_body = require("kong.tools.http").get_default_exit_body -- error codes http status codes local ERRORS_HTTP_CODES = { [Errors.codes.INVALID_PRIMARY_KEY] = 400, [Errors.codes.SCHEMA_VIOLATION] = 400, [Errors.codes.PRIMARY_KEY_VIOLATION] = 400, [Errors.codes.FOREIGN_KEY_VIOLATION] = 400, [Errors.codes.UNIQUE_VIOLATION] = 409, [Errors.codes.NOT_FOUND] = 404, [Errors.codes.INVALID_OFFSET] = 400, [Errors.codes.DATABASE_ERROR] = 500, [Errors.codes.INVALID_SIZE] = 400, [Errors.codes.INVALID_UNIQUE] = 400, [Errors.codes.INVALID_OPTIONS] = 400, [Errors.codes.OPERATION_UNSUPPORTED] = 405, [Errors.codes.FOREIGN_KEYS_UNRESOLVED] = 400, [Errors.codes.REFERENCED_BY_OTHERS] = 400, } local TAGS_AND_REGEX local TAGS_OR_REGEX do -- printable ASCII (0x20-0x7E except ','(0x2C) and '/'(0x2F), -- plus non-ASCII utf8 (0x80-0xF4) local tag_bytes = [[\x20-\x2B\x2D\x2E\x30-\x7E\x80-\xF4]] TAGS_AND_REGEX = "^([" .. tag_bytes .. "]+(?:,|$))+$" TAGS_OR_REGEX = "^([" .. tag_bytes .. "]+(?:/|$))+$" end local function get_message(default, ...) local message local n = select("#", ...) if n > 0 then if n == 1 then local arg = select(1, ...) if type(arg) == "table" then message = arg elseif arg ~= nil then message = tostring(arg) end else message = {} for i = 1, n do local arg = select(i, ...) message[i] = tostring(arg) end message = concat(message) end end if not message then message = default end if type(message) == "string" then message = { message = message } end return message end local function ok(...) return kong.response.exit(200, get_message(nil, ...)) end local function created(...) return kong.response.exit(201, get_message(nil, ...)) end local function no_content() return kong.response.exit(204) end local function not_found(...) return kong.response.exit(404, get_message("Not found", ...)) end local function method_not_allowed(...) return kong.response.exit(405, get_message("Method not allowed", ...)) end local function unexpected(...) return kong.response.exit(500, get_message("An unexpected error occurred", ...)) end local function handle_error(err_t) if type(err_t) ~= "table" then kong.log.err(err_t) return unexpected() end if err_t.strategy then err_t.strategy = nil end local status = ERRORS_HTTP_CODES[err_t.code] if not status or status == 500 then return app_helpers.yield_error(err_t) end if err_t.code == Errors.codes.OPERATION_UNSUPPORTED then return kong.response.exit(status, err_t) end return kong.response.exit(status, get_default_exit_body(status, err_t)) end local function extract_options(db, args, schema, context) local options = { nulls = true, pagination = { page_size = 100, max_page_size = 1000, }, workspace = workspaces.get_workspace_id(), } if args and schema and context then if schema.ttl == true and args.ttl ~= nil and (context == "insert" or context == "update" or context == "upsert") then options.ttl = args.ttl args.ttl = nil end if schema.fields.tags and args.tags ~= nil and context == "page" then if args.tags == null or #args.tags == 0 then local error_message = "cannot be null" return nil, error_message, db[schema.name].errors:invalid_options({tags = error_message}) end local tags = args.tags if type(tags) == "table" then tags = tags[1] end if re_match(tags, TAGS_AND_REGEX, 'jo') then -- 'a,b,c' or 'a' options.tags_cond = 'and' options.tags = splitn(tags, ',') elseif re_match(tags, TAGS_OR_REGEX, 'jo') then -- 'a/b/c' options.tags_cond = 'or' options.tags = splitn(tags, '/') else options.tags = tags -- not setting tags_cond since we can't determine the cond -- will raise an error in db/dao/init.lua:validate_options_value end args.tags = nil end end return options end local function get_page_size(args) local size = args.size if size ~= nil then size = tonumber(size) if size == nil then return nil, "size must be a number" end return size end end local function query_entity(context, self, db, schema, method) local is_insert = context == "insert" local is_update = context == "update" or context == "upsert" local args if is_update or is_insert then args = self.args.post else args = self.args.uri end local opts, err, err_t = extract_options(db, args, schema, context) if err then return nil, err, err_t end local schema_name = schema.name local dao = db[schema_name] if is_insert then return dao[method or context](dao, args, opts) end if context == "page" then local size, err = get_page_size(args) if err then return nil, err, db[schema_name].errors:invalid_size(err) end if not method then return dao[context](dao, size, args.offset, opts) end local key = self.params[schema_name] if schema_name == "tags" then key = unescape_uri(key) end return dao[method](dao, key, size, args.offset, opts) end local key = self.params[schema_name] if key then if type(key) ~= "table" then if type(key) == "string" then key = { id = unescape_uri(key) } else key = { id = key } end end if key.id and not uuid.is_valid_uuid(key.id) then local endpoint_key = schema.endpoint_key if endpoint_key then local field = schema.fields[endpoint_key] local inferred_value = arguments.infer_value(key.id, field) if is_update then return dao[method or context .. "_by_" .. endpoint_key](dao, inferred_value, args, opts) end return dao[method or context .. "_by_" .. endpoint_key](dao, inferred_value, opts) end end end if is_update then return dao[method or context](dao, key, args, opts) end return dao[method or context](dao, key, opts) end local function select_entity(...) return query_entity("select", ...) end local function update_entity(...) return query_entity("update", ...) end local function upsert_entity(...) return query_entity("upsert", ...) end local function delete_entity(...) return query_entity("delete", ...) end local function insert_entity(...) return query_entity("insert", ...) end local function page_collection(...) return query_entity("page", ...) end -- Generates admin api get collection endpoint functions -- -- Examples: -- -- /routes -- /services/:services/routes -- -- and -- -- /services local function get_collection_endpoint(schema, foreign_schema, foreign_field_name, method) return not foreign_schema and function(self, db, helpers) local next_page_tags = "" local next_page_size = "" local args = self.args.uri if args.tags then next_page_tags = "&tags=" .. escape_uri(type(args.tags) == "table" and args.tags[1] or args.tags) end if args.size then next_page_size = "&size=" .. args.size end local data, _, err_t, offset = page_collection(self, db, schema, method) if err_t then return handle_error(err_t) end local next_page = offset and fmt("/%s?offset=%s%s%s", schema.admin_api_name or schema.name, escape_uri(offset), next_page_tags, next_page_size) or null return ok { data = data, offset = offset, next = next_page, } end or function(self, db, helpers) local foreign_entity, _, err_t = select_entity(self, db, foreign_schema) if err_t then return handle_error(err_t) end if not foreign_entity then return not_found() end self.params[schema.name] = foreign_schema:extract_pk_values(foreign_entity) local method = method or "page_for_" .. foreign_field_name local data, _, err_t, offset = page_collection(self, db, schema, method) if err_t then return handle_error(err_t) end local foreign_key = self.params[foreign_schema.name] local next_page = offset and fmt("/%s/%s/%s?offset=%s", foreign_schema.admin_api_name or foreign_schema.name, foreign_key, schema.admin_api_nested_name or schema.admin_api_name or schema.name, escape_uri(offset)) or null return ok { data = data, offset = offset, next = next_page, } end end -- Generates admin api post collection endpoint functions -- -- Examples: -- -- /routes -- /services/:services/routes -- -- and -- -- /services local function post_collection_endpoint(schema, foreign_schema, foreign_field_name, method) return function(self, db, helpers, post_process) if foreign_schema then local foreign_entity, _, err_t = select_entity(self, db, foreign_schema) if err_t then return handle_error(err_t) end if not foreign_entity then return not_found() end self.args.post[foreign_field_name] = foreign_schema:extract_pk_values(foreign_entity) end local entity, _, err_t = insert_entity(self, db, schema, method) if err_t then return handle_error(err_t) end if post_process then entity, _, err_t = post_process(entity) if err_t then return handle_error(err_t) end end return created(entity) end end -- Generates admin api get entity endpoint functions -- -- Examples: -- -- /routes/:routes -- /routes/:routes/service -- -- and -- -- /services/:services -- /services/:services/routes/:routes local function get_entity_endpoint(schema, foreign_schema, foreign_field_name, method, is_foreign_entity_endpoint) return function(self, db, helpers) local entity, _, err_t if foreign_schema then entity, _, err_t = select_entity(self, db, schema) else entity, _, err_t = select_entity(self, db, schema, method) end if err_t then return handle_error(err_t) end if not entity then return not_found() end if foreign_schema then local pk if is_foreign_entity_endpoint then pk = entity[foreign_field_name] if not pk or pk == null then return not_found() end self.params[foreign_schema.name] = pk else pk = schema:extract_pk_values(entity) end entity, _, err_t = select_entity(self, db, foreign_schema, method) if err_t then return handle_error(err_t) end if not entity then return not_found() end if not is_foreign_entity_endpoint then local fk = entity[foreign_field_name] if not fk or fk == null then return not_found() end fk = schema:extract_pk_values(fk) for k, v in pairs(pk) do if fk[k] ~= v then return not_found() end end end end return ok(entity) end end -- Generates admin api put entity endpoint functions -- -- Examples: -- -- /routes/:routes -- /routes/:routes/service -- -- and -- -- /services/:services -- /services/:services/routes/:routes local function put_entity_endpoint(schema, foreign_schema, foreign_field_name, method, is_foreign_entity_endpoint) return not foreign_schema and function(self, db, helpers) local entity, _, err_t = upsert_entity(self, db, schema, method) if err_t then return handle_error(err_t) end if not entity then return not_found() end return ok(entity) end or function(self, db, helpers) local entity, _, err_t = select_entity(self, db, schema) if err_t then return handle_error(err_t) end if not entity then return not_found() end local associate if is_foreign_entity_endpoint then local pk = entity[foreign_field_name] if pk and pk ~= null then self.params[foreign_schema.name] = pk else associate = true self.params[foreign_schema.name] = uuid.uuid() end else self.args.post[foreign_field_name] = schema:extract_pk_values(entity) end local foreign_entity foreign_entity, _, err_t = upsert_entity(self, db, foreign_schema, method) if err_t then return handle_error(err_t) end if not foreign_entity then return not_found() end if associate then local pk = schema:extract_pk_values(entity) local data = { [foreign_field_name] = foreign_schema:extract_pk_values(foreign_entity) } _, _, err_t = db[schema.name]:update(pk, data) if err_t then return handle_error(err_t) end --if not entity then -- route was deleted after service was created, -- so we cannot associate anymore. perhaps not -- worth it to handle, the service on the other -- hand was updates just fine. --end end return ok(foreign_entity) end end -- Generates admin api patch entity endpoint functions -- -- Examples: -- -- /routes/:routes -- /routes/:routes/service -- -- and -- -- /services/:services -- /services/:services/routes/:routes local function patch_entity_endpoint(schema, foreign_schema, foreign_field_name, method, is_foreign_entity_endpoint) return not foreign_schema and function(self, db, helpers) local entity, _, err_t = update_entity(self, db, schema, method) if err_t then return handle_error(err_t) end if not entity then return not_found() end return ok(entity) end or function(self, db, helpers) local entity, _, err_t = select_entity(self, db, schema) if err_t then return handle_error(err_t) end if not entity then return not_found() end if is_foreign_entity_endpoint then local pk = entity[foreign_field_name] if not pk or pk == null then return not_found() end self.params[foreign_schema.name] = pk else if not self.args.post[foreign_field_name] then self.args.post[foreign_field_name] = schema:extract_pk_values(entity) end local pk = schema:extract_pk_values(entity) entity, _, err_t = select_entity(self, db, foreign_schema) if err_t then return handle_error(err_t) end if not entity then return not_found() end local fk = entity[foreign_field_name] if not fk or fk == null then return not_found() end fk = schema:extract_pk_values(fk) for k, v in pairs(pk) do if fk[k] ~= v then return not_found() end end end entity, _, err_t = update_entity(self, db, foreign_schema, method) if err_t then return handle_error(err_t) end if not entity then return not_found() end return ok(entity) end end -- Generates admin api delete entity endpoint functions -- -- Examples: -- -- /routes/:routes -- /routes/:routes/service -- -- and -- -- /services/:services -- /services/:services/routes/:routes local function delete_entity_endpoint(schema, foreign_schema, foreign_field_name, method, is_foreign_entity_endpoint) return not foreign_schema and function(self, db, helpers) local _, _, err_t = delete_entity(self, db, schema, method) if err_t then return handle_error(err_t) end return no_content() end or function(self, db, helpers) local entity, _, err_t = select_entity(self, db, schema) if err_t then return handle_error(err_t) end if not entity then return not_found() end if is_foreign_entity_endpoint then local id = entity[foreign_field_name] if not id or id == null then return not_found() end return method_not_allowed() end local pk = schema:extract_pk_values(entity) entity, _, err_t = select_entity(self, db, foreign_schema) if err_t then return handle_error(err_t) end if not entity then return not_found() end local fk = entity[foreign_field_name] if not fk or fk == null then return not_found() end fk = schema:extract_pk_values(fk) for k, v in pairs(pk) do if fk[k] ~= v then return not_found() end end local _, _, err_t = delete_entity(self, db, foreign_schema, method) if err_t then return handle_error(err_t) end return no_content() end end -- Generates admin api collection endpoint functions -- -- Examples: -- -- /routes -- /services/:services/routes -- -- and -- -- /services local function generate_collection_endpoints(endpoints, schema, foreign_schema, foreign_field_name) local collection_path if foreign_schema then collection_path = fmt("/%s/:%s/%s", foreign_schema.admin_api_name or foreign_schema.name, foreign_schema.name, schema.admin_api_nested_name or schema.admin_api_name or schema.name) else collection_path = fmt("/%s", schema.admin_api_name or schema.name) end endpoints[collection_path] = { schema = schema, methods = { --OPTIONS = method_not_allowed, --HEAD = method_not_allowed, GET = get_collection_endpoint(schema, foreign_schema, foreign_field_name), POST = post_collection_endpoint(schema, foreign_schema, foreign_field_name), --PUT = method_not_allowed, --PATCH = method_not_allowed, --DELETE = method_not_allowed, }, } end -- Generates admin api entity endpoint functions -- -- Examples: -- -- /routes/:routes -- /routes/:routes/service -- -- and -- -- /services/:services -- /services/:services/routes/:routes local function generate_entity_endpoints(endpoints, schema, foreign_schema, foreign_field_name, is_foreign_entity_endpoint) local entity_path if foreign_schema then if is_foreign_entity_endpoint then entity_path = fmt("/%s/:%s/%s", schema.admin_api_name or schema.name, schema.name, foreign_field_name) else entity_path = fmt("/%s/:%s/%s/:%s", schema.admin_api_name or schema.name, schema.name, foreign_schema.admin_api_nested_name or foreign_schema.admin_api_name or foreign_schema.name, foreign_schema.name) end else entity_path = fmt("/%s/:%s", schema.admin_api_name or schema.name, schema.name) end endpoints[entity_path] = { schema = foreign_schema or schema, methods = { --OPTIONS = method_not_allowed, --HEAD = method_not_allowed, GET = get_entity_endpoint(schema, foreign_schema, foreign_field_name, nil, is_foreign_entity_endpoint), --POST = method_not_allowed, PUT = put_entity_endpoint(schema, foreign_schema, foreign_field_name, nil, is_foreign_entity_endpoint), PATCH = patch_entity_endpoint(schema, foreign_schema, foreign_field_name, nil, is_foreign_entity_endpoint), DELETE = delete_entity_endpoint(schema, foreign_schema, foreign_field_name, nil, is_foreign_entity_endpoint), }, } end -- Generates admin api endpoint functions -- -- Examples: -- -- /routes -- /routes/:routes -- /routes/:routes/service -- /services/:services/routes -- -- and -- -- /services -- /services/:services -- /services/:services/routes/:routes local function generate_endpoints(schema, endpoints) -- e.g. /routes generate_collection_endpoints(endpoints, schema) -- e.g. /routes/:routes generate_entity_endpoints(endpoints, schema) for foreign_field_name, foreign_field in schema:each_field() do if foreign_field.type == "foreign" then -- e.g. /routes/:routes/service generate_entity_endpoints(endpoints, schema, foreign_field.schema, foreign_field_name, true) -- e.g. /services/:services/routes generate_collection_endpoints(endpoints, schema, foreign_field.schema, foreign_field_name) -- e.g. /services/:services/routes/:routes generate_entity_endpoints(endpoints, foreign_field.schema, schema, foreign_field_name) end end return endpoints end -- A reusable handler for endpoints that are deactivated -- (e.g. /targets/:targets) local disable = { before = function() return not_found() end } local Endpoints = { disable = disable, handle_error = handle_error, get_page_size = get_page_size, extract_options = extract_options, select_entity = select_entity, update_entity = update_entity, upsert_entity = upsert_entity, delete_entity = delete_entity, insert_entity = insert_entity, page_collection = page_collection, get_entity_endpoint = get_entity_endpoint, put_entity_endpoint = put_entity_endpoint, patch_entity_endpoint = patch_entity_endpoint, delete_entity_endpoint = delete_entity_endpoint, get_collection_endpoint = get_collection_endpoint, post_collection_endpoint = post_collection_endpoint, } function Endpoints.new(schema, endpoints) return generate_endpoints(schema, endpoints) end return Endpoints