Files
2026-07-13 12:32:21 +08:00

897 lines
22 KiB
Lua

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