diff --git a/bun.lockb b/bun.lockb index 62eaa29..803fa0b 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/client/crud-fetch/index.ts b/client/crud-fetch/index.ts new file mode 100644 index 0000000..950518d --- /dev/null +++ b/client/crud-fetch/index.ts @@ -0,0 +1,55 @@ +import path from "path"; +import { + APIResponseObject, + ClientCrudFetchParams, + PostInsertReturn, +} from "../../package-shared/types"; +import serializeQuery from "../../package-shared/utils/serialize-query"; +import fetchApi from "../fetch"; + +export default async function clientCrudFetch< + T extends { [k: string]: any } = { [k: string]: any }, + P = string, + R extends { [k: string]: any } = { [k: string]: any } +>({ + table, + basePath, + body, + query, + targetId, + method = "GET", + apiOrigin, +}: ClientCrudFetchParams) { + try { + let pathname = basePath || ``; + + pathname = path.join(pathname, String(table)); + + if (targetId) { + pathname = path.join(pathname, String(targetId)); + } + + if (query) { + pathname = `${pathname}${serializeQuery(query)}`; + } + + pathname = apiOrigin + ? `${apiOrigin}/${pathname}`.replace(/([^:]\/)\/+/g, "$1") + : pathname; + + const res = await fetchApi< + any, + APIResponseObject + >(pathname, { + method, + body, + }); + + return res; + } catch (error: any) { + return { + success: false, + msg: `API ERROR => ${error.message}`, + }; + } +} diff --git a/client/index.ts b/client/index.ts index c7771cd..4d0aec3 100644 --- a/client/index.ts +++ b/client/index.ts @@ -14,6 +14,7 @@ import slugify from "../package-shared/utils/slugify"; import postLogin from "./auth/post-login"; import deserializeQuery from "../package-shared/utils/deserialize-query"; import debugLog from "../package-shared/utils/logging/debug-log"; +import clientCrudFetch from "./crud-fetch"; const media = { imageInputToBase64, @@ -56,6 +57,12 @@ const fetch = { /** * Main Export */ -const datasquirelClient = { media, auth, fetch, utils }; +const datasquirelClient = { + media, + auth, + fetch, + utils, + clientCrudFetch, +}; export default datasquirelClient; diff --git a/dist/client/crud-fetch/index.d.ts b/dist/client/crud-fetch/index.d.ts new file mode 100644 index 0000000..d0b8635 --- /dev/null +++ b/dist/client/crud-fetch/index.d.ts @@ -0,0 +1,10 @@ +import { APIResponseObject, ClientCrudFetchParams, PostInsertReturn } from "../../package-shared/types"; +export default function clientCrudFetch({ table, basePath, body, query, targetId, method, apiOrigin, }: ClientCrudFetchParams): Promise>; diff --git a/dist/client/crud-fetch/index.js b/dist/client/crud-fetch/index.js new file mode 100644 index 0000000..706ff7d --- /dev/null +++ b/dist/client/crud-fetch/index.js @@ -0,0 +1,46 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = clientCrudFetch; +const path_1 = __importDefault(require("path")); +const serialize_query_1 = __importDefault(require("../../package-shared/utils/serialize-query")); +const fetch_1 = __importDefault(require("../fetch")); +function clientCrudFetch(_a) { + return __awaiter(this, arguments, void 0, function* ({ table, basePath, body, query, targetId, method = "GET", apiOrigin, }) { + try { + let pathname = basePath || ``; + pathname = path_1.default.join(pathname, String(table)); + if (targetId) { + pathname = path_1.default.join(pathname, String(targetId)); + } + if (query) { + pathname = `${pathname}${(0, serialize_query_1.default)(query)}`; + } + pathname = apiOrigin + ? `${apiOrigin}/${pathname}`.replace(/([^:]\/)\/+/g, "$1") + : pathname; + const res = yield (0, fetch_1.default)(pathname, { + method, + body, + }); + return res; + } + catch (error) { + return { + success: false, + msg: `API ERROR => ${error.message}`, + }; + } + }); +} diff --git a/dist/client/index.d.ts b/dist/client/index.d.ts index 7020fcd..ecdfa25 100644 --- a/dist/client/index.d.ts +++ b/dist/client/index.d.ts @@ -12,6 +12,7 @@ import slugify from "../package-shared/utils/slugify"; import postLogin from "./auth/post-login"; import deserializeQuery from "../package-shared/utils/deserialize-query"; import debugLog from "../package-shared/utils/logging/debug-log"; +import clientCrudFetch from "./crud-fetch"; /** * Main Export */ @@ -51,5 +52,6 @@ declare const datasquirelClient: { slugify: typeof slugify; debugLog: typeof debugLog; }; + clientCrudFetch: typeof clientCrudFetch; }; export default datasquirelClient; diff --git a/dist/client/index.js b/dist/client/index.js index 680a86b..3b3c380 100644 --- a/dist/client/index.js +++ b/dist/client/index.js @@ -19,6 +19,7 @@ const slugify_1 = __importDefault(require("../package-shared/utils/slugify")); const post_login_1 = __importDefault(require("./auth/post-login")); const deserialize_query_1 = __importDefault(require("../package-shared/utils/deserialize-query")); const debug_log_1 = __importDefault(require("../package-shared/utils/logging/debug-log")); +const crud_fetch_1 = __importDefault(require("./crud-fetch")); const media = { imageInputToBase64: imageInputToBase64_1.default, imageInputFileToBase64: imageInputFileToBase64_1.default, @@ -56,5 +57,11 @@ const fetch = { /** * Main Export */ -const datasquirelClient = { media, auth, fetch, utils }; +const datasquirelClient = { + media, + auth, + fetch, + utils, + clientCrudFetch: crud_fetch_1.default, +}; exports.default = datasquirelClient; diff --git a/dist/index.d.ts b/dist/index.d.ts index fc98f0f..6b9c2c8 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -24,6 +24,7 @@ import handleNodemailer from "./package-shared/functions/backend/handleNodemaile import grabDSQLConnection from "./package-shared/utils/grab-dsql-connection"; import grabDSQLConnectionConfig from "./package-shared/utils/grab-dsql-connection-config"; import purgeDefaultFields from "./package-shared/utils/purge-default-fields"; +import apiCrudHandler from "./package-shared/api-paths"; /** * Main Export */ @@ -121,6 +122,7 @@ declare const datasquirel: { slugify: typeof import("./package-shared/utils/slugify").default; debugLog: typeof debugLog; }; + clientCrudFetch: typeof import("./client/crud-fetch").default; }; sql: { sqlGenerator: typeof sqlGenerator; @@ -164,5 +166,6 @@ declare const datasquirel: { mail: { mailer: typeof handleNodemailer; }; + apiCrudHandler: typeof apiCrudHandler; }; export default datasquirel; diff --git a/dist/index.js b/dist/index.js index 942471a..791634c 100644 --- a/dist/index.js +++ b/dist/index.js @@ -31,6 +31,7 @@ const grab_dsql_connection_1 = __importDefault(require("./package-shared/utils/g const grab_dsql_connection_config_1 = __importDefault(require("./package-shared/utils/grab-dsql-connection-config")); const schema_1 = __importDefault(require("./package-shared/api/schema")); const purge_default_fields_1 = __importDefault(require("./package-shared/utils/purge-default-fields")); +const api_paths_1 = __importDefault(require("./package-shared/api-paths")); /** * API Functions Object */ @@ -105,5 +106,6 @@ const datasquirel = { mail: { mailer: handleNodemailer_1.default, }, + apiCrudHandler: api_paths_1.default, }; exports.default = datasquirel; diff --git a/dist/package-shared/api-paths/functions/delete-result.d.ts b/dist/package-shared/api-paths/functions/delete-result.d.ts new file mode 100644 index 0000000..16829a9 --- /dev/null +++ b/dist/package-shared/api-paths/functions/delete-result.d.ts @@ -0,0 +1,6 @@ +import { APIPathsCrudParams, APIResponseObject } from "../../types"; +export default function ({ table, body, targetId, }: APIPathsCrudParams): Promise; diff --git a/dist/package-shared/api-paths/functions/delete-result.js b/dist/package-shared/api-paths/functions/delete-result.js new file mode 100644 index 0000000..bb6b08d --- /dev/null +++ b/dist/package-shared/api-paths/functions/delete-result.js @@ -0,0 +1,25 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = default_1; +const crud_1 = __importDefault(require("../../utils/data-fetching/crud")); +function default_1(_a) { + return __awaiter(this, arguments, void 0, function* ({ table, body, targetId, }) { + if (!targetId && !(body === null || body === void 0 ? void 0 : body.searchQuery)) { + throw new Error(`Target ID or \`searchQuery\` field is required to delete Data.`); + } + const DELETE_RESLT = yield (0, crud_1.default)(Object.assign(Object.assign({}, body === null || body === void 0 ? void 0 : body.crudParams), { action: "delete", table, query: body === null || body === void 0 ? void 0 : body.searchQuery, targetId })); + return DELETE_RESLT; + }); +} diff --git a/dist/package-shared/api-paths/functions/get-result.d.ts b/dist/package-shared/api-paths/functions/get-result.d.ts new file mode 100644 index 0000000..84cbf4f --- /dev/null +++ b/dist/package-shared/api-paths/functions/get-result.d.ts @@ -0,0 +1,6 @@ +import { APIPathsCrudParams, APIResponseObject } from "../../types"; +export default function ({ table, query, allowedTable, url, }: APIPathsCrudParams): Promise; diff --git a/dist/package-shared/api-paths/functions/get-result.js b/dist/package-shared/api-paths/functions/get-result.js new file mode 100644 index 0000000..1483288 --- /dev/null +++ b/dist/package-shared/api-paths/functions/get-result.js @@ -0,0 +1,27 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = default_1; +const crud_1 = __importDefault(require("../../utils/data-fetching/crud")); +function default_1(_a) { + return __awaiter(this, arguments, void 0, function* ({ table, query, allowedTable, url, }) { + var _b, _c; + if (((allowedTable === null || allowedTable === void 0 ? void 0 : allowedTable.allowedFields) || (allowedTable === null || allowedTable === void 0 ? void 0 : allowedTable.disallowedFields)) && + !((_c = (_b = query === null || query === void 0 ? void 0 : query.searchQuery) === null || _b === void 0 ? void 0 : _b.selectFields) === null || _c === void 0 ? void 0 : _c[0])) { + throw new Error(`Please specify fields to select! Use the \`selectFields\` option in the query object`); + } + const GET_RESULT = yield (0, crud_1.default)(Object.assign(Object.assign({}, query === null || query === void 0 ? void 0 : query.crudParams), { action: "get", table, query: query === null || query === void 0 ? void 0 : query.searchQuery })); + return GET_RESULT; + }); +} diff --git a/dist/package-shared/api-paths/functions/post-result.d.ts b/dist/package-shared/api-paths/functions/post-result.d.ts new file mode 100644 index 0000000..1426945 --- /dev/null +++ b/dist/package-shared/api-paths/functions/post-result.d.ts @@ -0,0 +1,6 @@ +import { APIPathsCrudParams, APIResponseObject } from "../../types"; +export default function ({ table, body }: APIPathsCrudParams): Promise; diff --git a/dist/package-shared/api-paths/functions/post-result.js b/dist/package-shared/api-paths/functions/post-result.js new file mode 100644 index 0000000..4e0ddc6 --- /dev/null +++ b/dist/package-shared/api-paths/functions/post-result.js @@ -0,0 +1,22 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = default_1; +const crud_1 = __importDefault(require("../../utils/data-fetching/crud")); +function default_1(_a) { + return __awaiter(this, arguments, void 0, function* ({ table, body }) { + const POST_RESULT = yield (0, crud_1.default)(Object.assign(Object.assign({}, body === null || body === void 0 ? void 0 : body.crudParams), { action: "insert", table, data: body === null || body === void 0 ? void 0 : body.data })); + return POST_RESULT; + }); +} diff --git a/dist/package-shared/api-paths/functions/put-result.d.ts b/dist/package-shared/api-paths/functions/put-result.d.ts new file mode 100644 index 0000000..16829a9 --- /dev/null +++ b/dist/package-shared/api-paths/functions/put-result.d.ts @@ -0,0 +1,6 @@ +import { APIPathsCrudParams, APIResponseObject } from "../../types"; +export default function ({ table, body, targetId, }: APIPathsCrudParams): Promise; diff --git a/dist/package-shared/api-paths/functions/put-result.js b/dist/package-shared/api-paths/functions/put-result.js new file mode 100644 index 0000000..04147d6 --- /dev/null +++ b/dist/package-shared/api-paths/functions/put-result.js @@ -0,0 +1,25 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = default_1; +const crud_1 = __importDefault(require("../../utils/data-fetching/crud")); +function default_1(_a) { + return __awaiter(this, arguments, void 0, function* ({ table, body, targetId, }) { + if (!targetId && !(body === null || body === void 0 ? void 0 : body.searchQuery)) { + throw new Error(`Target ID or \`searchQuery\` field is required to update Data.`); + } + const PUT_RESULT = yield (0, crud_1.default)(Object.assign(Object.assign({}, body === null || body === void 0 ? void 0 : body.crudParams), { action: "update", table, data: body === null || body === void 0 ? void 0 : body.data, query: body === null || body === void 0 ? void 0 : body.searchQuery, targetId })); + return PUT_RESULT; + }); +} diff --git a/dist/package-shared/api-paths/index.d.ts b/dist/package-shared/api-paths/index.d.ts new file mode 100644 index 0000000..31d5f0f --- /dev/null +++ b/dist/package-shared/api-paths/index.d.ts @@ -0,0 +1,6 @@ +import { APIPathsParams, APIResponseObject } from "../types"; +export default function apiCrudHandler(params: APIPathsParams): Promise; diff --git a/dist/package-shared/api-paths/index.js b/dist/package-shared/api-paths/index.js new file mode 100644 index 0000000..c21258d --- /dev/null +++ b/dist/package-shared/api-paths/index.js @@ -0,0 +1,71 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = apiCrudHandler; +const lodash_1 = __importDefault(require("lodash")); +const get_result_1 = __importDefault(require("./functions/get-result")); +const grab_path_data_1 = require("./utils/grab-path-data"); +const checks_1 = __importDefault(require("./utils/checks")); +const post_result_1 = __importDefault(require("./functions/post-result")); +const put_result_1 = __importDefault(require("./functions/put-result")); +const delete_result_1 = __importDefault(require("./functions/delete-result")); +function apiCrudHandler(params) { + return __awaiter(this, void 0, void 0, function* () { + try { + const { auth, method } = params; + const isAuthorized = yield (auth === null || auth === void 0 ? void 0 : auth()); + const { table, targetId, url, query } = (0, grab_path_data_1.grabPathData)(params); + const crudParams = Object.assign(Object.assign({}, params), { isAuthorized, + table, + targetId }); + const checkedObj = yield (0, checks_1.default)(Object.assign(Object.assign({}, crudParams), { query: lodash_1.default.merge(params.query || {}, query) })); + crudParams.query = checkedObj.query; + crudParams.body = checkedObj.body; + crudParams.allowedTable = checkedObj.allowedTable; + crudParams.url = url; + if (targetId) { + if (crudParams.query) { + if (crudParams.query.crudParams) { + crudParams.query.crudParams.targetId = targetId; + } + else { + crudParams.query.crudParams = { + targetId, + }; + } + } + } + switch (method) { + case "GET": + return yield (0, get_result_1.default)(crudParams); + case "POST": + return yield (0, post_result_1.default)(crudParams); + case "PUT": + return yield (0, put_result_1.default)(crudParams); + case "DELETE": + return yield (0, delete_result_1.default)(crudParams); + } + return { + success: false, + msg: `Unhandled`, + }; + } + catch (error) { + return { + success: false, + msg: error.message, + }; + } + }); +} diff --git a/dist/package-shared/api-paths/utils/checks.d.ts b/dist/package-shared/api-paths/utils/checks.d.ts new file mode 100644 index 0000000..a938e31 --- /dev/null +++ b/dist/package-shared/api-paths/utils/checks.d.ts @@ -0,0 +1,8 @@ +import { APIPathsCrudParams, APIPathsParamsAllowedTable } from "../../types"; +export default function checks({ table, allowedTables, query, body, method, getMiddleware, postMiddleware, putMiddleware, deleteMiddleware, crudMiddleware, }: APIPathsCrudParams): Promise, "query" | "body"> & { + allowedTable: APIPathsParamsAllowedTable; +}>; diff --git a/dist/package-shared/api-paths/utils/checks.js b/dist/package-shared/api-paths/utils/checks.js new file mode 100644 index 0000000..a069251 --- /dev/null +++ b/dist/package-shared/api-paths/utils/checks.js @@ -0,0 +1,121 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = checks; +const lodash_1 = __importDefault(require("lodash")); +function checks(_a) { + return __awaiter(this, arguments, void 0, function* ({ table, allowedTables, query, body, method, getMiddleware, postMiddleware, putMiddleware, deleteMiddleware, crudMiddleware, }) { + var _b, _c, _d, _e; + const allowedTable = allowedTables.find((tbl) => tbl.table == table); + if (!allowedTable) { + throw new Error(`Can't Access this table: \`${table}\``); + } + let newQuery = lodash_1.default.cloneDeep(query); + let newBody = lodash_1.default.cloneDeep(body); + const searchFields = Object.keys(((_b = newQuery === null || newQuery === void 0 ? void 0 : newQuery.searchQuery) === null || _b === void 0 ? void 0 : _b.query) || {}); + const selectFields = (_d = (((_c = newQuery === null || newQuery === void 0 ? void 0 : newQuery.searchQuery) === null || _c === void 0 ? void 0 : _c.selectFields) + ? newQuery.searchQuery.selectFields.map((f) => typeof f == "string" + ? f + : typeof f == "object" + ? f.fieldName + : undefined) + : undefined)) === null || _d === void 0 ? void 0 : _d.filter((f) => typeof f == "string"); + const targetFields = [...(searchFields || []), ...(selectFields || [])]; + if (method == "GET" && allowedTable.allowedFields) { + for (let i = 0; i < targetFields.length; i++) { + const fld = targetFields[i]; + const allowedFld = allowedTable.allowedFields.find((f) => typeof f == "string" ? f == fld : fld.match(f)); + if (!allowedFld) { + throw new Error(`\`${allowedFld}\` field not allowed`); + } + } + } + if (method == "GET" && allowedTable.disallowedFields) { + for (let i = 0; i < targetFields.length; i++) { + const fld = targetFields[i]; + const disallowedFld = allowedTable.disallowedFields.find((f) => typeof f == "string" ? f == fld : fld.match(f)); + if (disallowedFld) { + throw new Error(`\`${disallowedFld}\` field not allowed`); + } + } + } + if (method == "GET" && getMiddleware) { + newQuery = yield getMiddleware({ query: newQuery || {} }); + } + if (method !== "GET" && crudMiddleware) { + const middRes = yield crudMiddleware({ + body: newBody || {}, + query: newQuery || {}, + }); + newBody = lodash_1.default.merge(newBody, middRes); + } + if (method == "POST" && postMiddleware) { + const middRes = yield postMiddleware({ + body: newBody || {}, + query: newQuery || {}, + }); + newBody = lodash_1.default.merge(newBody, middRes); + } + if (method == "PUT" && putMiddleware) { + const middRes = yield putMiddleware({ + body: newBody || {}, + query: newQuery || {}, + }); + newBody = lodash_1.default.merge(newBody, middRes); + } + if (method == "DELETE" && deleteMiddleware) { + const middRes = yield deleteMiddleware({ + body: newBody || {}, + query: newQuery || {}, + }); + newBody = lodash_1.default.merge(newBody, middRes); + } + if ((_e = newQuery === null || newQuery === void 0 ? void 0 : newQuery.searchQuery) === null || _e === void 0 ? void 0 : _e.join) { + for (let i = 0; i < newQuery.searchQuery.join.length; i++) { + const join = newQuery.searchQuery.join[i]; + const joinTableName = join.tableName; + const selectFields = join.selectFields; + if (allowedTables === null || allowedTables === void 0 ? void 0 : allowedTables[0]) { + const allowedJoinTable = allowedTables.find((t) => t.table == joinTableName); + if (!(allowedJoinTable === null || allowedJoinTable === void 0 ? void 0 : allowedJoinTable.table)) { + throw new Error(`Can't joint \`${joinTableName}\` table`); + } + const allowedFields = allowedJoinTable.allowedFields; + const disallowedFields = allowedJoinTable.disallowedFields; + if (selectFields === null || selectFields === void 0 ? void 0 : selectFields[0]) { + for (let j = 0; j < selectFields.length; j++) { + const selectField = selectFields[j]; + const selectFieldName = typeof selectField == "object" + ? selectField.field + : String(selectField); + if ((allowedFields === null || allowedFields === void 0 ? void 0 : allowedFields[0]) && + !allowedFields.find((f) => String(f) == selectFieldName)) { + throw new Error(`Can't Select this Field!`); + } + if ((disallowedFields === null || disallowedFields === void 0 ? void 0 : disallowedFields[0]) && + disallowedFields.find((f) => String(f) == selectFieldName)) { + throw new Error(`Disallowed Field Selected!`); + } + } + } + } + } + } + return { + query: newQuery, + body: newBody, + allowedTable, + }; + }); +} diff --git a/dist/package-shared/api-paths/utils/grab-path-data.d.ts b/dist/package-shared/api-paths/utils/grab-path-data.d.ts new file mode 100644 index 0000000..871d1ba --- /dev/null +++ b/dist/package-shared/api-paths/utils/grab-path-data.d.ts @@ -0,0 +1,6 @@ +import { APIPathsData, APIPathsParams } from "../../types"; +export declare function grabPathData({ href, basePath }: APIPathsParams): APIPathsData; diff --git a/dist/package-shared/api-paths/utils/grab-path-data.js b/dist/package-shared/api-paths/utils/grab-path-data.js new file mode 100644 index 0000000..fd1d6ed --- /dev/null +++ b/dist/package-shared/api-paths/utils/grab-path-data.js @@ -0,0 +1,28 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.grabPathData = grabPathData; +const deserialize_query_1 = __importDefault(require("../../utils/deserialize-query")); +function grabPathData({ href, basePath }) { + const urlObj = new URL(href); + const pathname = basePath + ? urlObj.pathname.replace(basePath, "") + : urlObj.pathname; + const urlArray = pathname.split("/").filter((u) => Boolean(u.match(/./))); + const table = urlArray[0]; + const targetId = urlArray[1]; + let query = ((urlObj === null || urlObj === void 0 ? void 0 : urlObj.searchParams) + ? (0, deserialize_query_1.default)(Object.fromEntries(urlObj.searchParams)) + : undefined); + if (!table) { + throw new Error(`No Table Found`); + } + return { + table, + targetId, + query, + url: urlObj, + }; +} diff --git a/dist/package-shared/functions/backend/db/deleteDbEntry.d.ts b/dist/package-shared/functions/backend/db/deleteDbEntry.d.ts index b1b20c7..bc9f36b 100644 --- a/dist/package-shared/functions/backend/db/deleteDbEntry.d.ts +++ b/dist/package-shared/functions/backend/db/deleteDbEntry.d.ts @@ -1,4 +1,4 @@ -import { DSQL_TableSchemaType, PostInsertReturn } from "../../../types"; +import { APIResponseObject, DSQL_TableSchemaType, DsqlCrudParamWhereClause, PostInsertReturn } from "../../../types"; import { DbContextsArray } from "./runQuery"; type Param({ dbContext, dbFullName, tableName, identifierColumnName, identifierValue, forceLocal, }: Param): Promise; +} = any, K extends string = string>({ dbContext, dbFullName, tableName, identifierColumnName, identifierValue, forceLocal, whereClauseObject, }: Param): Promise>; export {}; diff --git a/dist/package-shared/functions/backend/db/deleteDbEntry.js b/dist/package-shared/functions/backend/db/deleteDbEntry.js index 417d19e..e313b27 100644 --- a/dist/package-shared/functions/backend/db/deleteDbEntry.js +++ b/dist/package-shared/functions/backend/db/deleteDbEntry.js @@ -13,6 +13,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = deleteDbEntry; +const sql_formatter_1 = require("sql-formatter"); const check_if_is_master_1 = __importDefault(require("../../../utils/check-if-is-master")); const conn_db_handler_1 = __importDefault(require("../../../utils/db/conn-db-handler")); /** @@ -20,7 +21,7 @@ const conn_db_handler_1 = __importDefault(require("../../../utils/db/conn-db-han * @description */ function deleteDbEntry(_a) { - return __awaiter(this, arguments, void 0, function* ({ dbContext, dbFullName, tableName, identifierColumnName, identifierValue, forceLocal, }) { + return __awaiter(this, arguments, void 0, function* ({ dbContext, dbFullName, tableName, identifierColumnName, identifierValue, forceLocal, whereClauseObject, }) { try { const isMaster = forceLocal ? true @@ -30,19 +31,42 @@ function deleteDbEntry(_a) { * * @description */ - const query = `DELETE FROM ${isMaster && !dbFullName ? "" : `\`${dbFullName}\`.`}\`${tableName}\` WHERE \`${identifierColumnName.toString()}\`=?`; - const deletedEntry = yield (0, conn_db_handler_1.default)({ + let query = `DELETE FROM ${isMaster && !dbFullName ? "" : `\`${dbFullName}\`.`}\`${tableName}\``; + let values = []; + if (whereClauseObject) { + query += ` ${whereClauseObject.clause}`; + values.push(...(whereClauseObject.params || [])); + } + else if (identifierColumnName && identifierValue) { + query += ` WHERE \`${identifierColumnName.toString()}\`=?`; + values = [identifierValue]; + } + else { + throw new Error(`Delete operation has no specified rows! Can't delete everything in this table!`); + } + const deletedEntry = (yield (0, conn_db_handler_1.default)({ query, - values: [identifierValue], - }); + values, + })); /** * Return statement */ - return deletedEntry; + return { + success: Boolean(deletedEntry.affectedRows), + payload: deletedEntry, + queryObject: { + sql: (0, sql_formatter_1.format)(query), + params: values, + }, + }; } catch (error) { - console.log("Error Deleting Entry =>", error.message); - return null; + const errorMsg = `Error Deleting Entry =>, ${error.message}`; + console.log(errorMsg); + return { + success: false, + msg: errorMsg, + }; } }); } diff --git a/dist/package-shared/functions/backend/db/grab-parsed-value.js b/dist/package-shared/functions/backend/db/grab-parsed-value.js index 127f5c4..a1349fc 100644 --- a/dist/package-shared/functions/backend/db/grab-parsed-value.js +++ b/dist/package-shared/functions/backend/db/grab-parsed-value.js @@ -62,7 +62,7 @@ function grabParsedValue({ value, tableSchema, encryptionKey, encryptionSalt, da } if (typeof newValue === "string" && (newValue.match(/^null$/i) || !newValue.match(/./i))) { - newValue = undefined; + newValue = ""; } if (typeof newValue === "boolean" || ((_d = targetFieldSchema === null || targetFieldSchema === void 0 ? void 0 : targetFieldSchema.dataType) === null || _d === void 0 ? void 0 : _d.match(/boolean/i))) { diff --git a/dist/package-shared/functions/backend/db/updateDbEntry.d.ts b/dist/package-shared/functions/backend/db/updateDbEntry.d.ts index 5759f57..8b76a40 100644 --- a/dist/package-shared/functions/backend/db/updateDbEntry.d.ts +++ b/dist/package-shared/functions/backend/db/updateDbEntry.d.ts @@ -1,5 +1,5 @@ import { DbContextsArray } from "./runQuery"; -import { APIResponseObject, DSQL_TableSchemaType, PostInsertReturn } from "../../../types"; +import { APIResponseObject, DSQL_TableSchemaType, DsqlCrudParamWhereClause, PostInsertReturn } from "../../../types"; import { ConnectionConfig } from "mariadb"; type Param({ dbContext, dbFullName, tableName, data, tableSchema, identifierColumnName, identifierValue, encryptionKey, encryptionSalt, forceLocal, debug, dbConfig, }: Param): Promise>; +} = any>({ dbContext, dbFullName, tableName, data, tableSchema, identifierColumnName, identifierValue, encryptionKey, encryptionSalt, forceLocal, debug, dbConfig, whereClauseObject, }: Param): Promise>; export {}; diff --git a/dist/package-shared/functions/backend/db/updateDbEntry.js b/dist/package-shared/functions/backend/db/updateDbEntry.js index c2d05fa..e97f4bc 100644 --- a/dist/package-shared/functions/backend/db/updateDbEntry.js +++ b/dist/package-shared/functions/backend/db/updateDbEntry.js @@ -24,7 +24,7 @@ const grab_parsed_value_1 = __importDefault(require("./grab-parsed-value")); * @description */ function updateDbEntry(_a) { - return __awaiter(this, arguments, void 0, function* ({ dbContext, dbFullName, tableName, data, tableSchema, identifierColumnName, identifierValue, encryptionKey, encryptionSalt, forceLocal, debug, dbConfig, }) { + return __awaiter(this, arguments, void 0, function* ({ dbContext, dbFullName, tableName, data, tableSchema, identifierColumnName, identifierValue, encryptionKey, encryptionSalt, forceLocal, debug, dbConfig, whereClauseObject, }) { var _b, _c; /** * Check if data is valid @@ -96,8 +96,15 @@ function updateDbEntry(_a) { updateKeyValueArray.push(`date_updated_code='${Date.now()}'`); //////////////////////////////////////// //////////////////////////////////////// - const query = `UPDATE ${isMaster && !dbFullName ? "" : `\`${dbFullName}\`.`}\`${tableName}\` SET ${updateKeyValueArray.join(",")} WHERE \`${identifierColumnName}\`=?`; - updateValues.push(identifierValue); + let query = `UPDATE ${isMaster && !dbFullName ? "" : `\`${dbFullName}\`.`}\`${tableName}\` SET ${updateKeyValueArray.join(",")}`; + if (whereClauseObject) { + query += ` ${whereClauseObject.clause}`; + updateValues.push(...(whereClauseObject.params || [])); + } + else { + query += ` WHERE \`${identifierColumnName}\`=?`; + updateValues.push(identifierValue); + } const updatedEntry = yield (0, conn_db_handler_1.default)({ query, values: updateValues, diff --git a/dist/package-shared/types/index.d.ts b/dist/package-shared/types/index.d.ts index 1467fab..e0861f6 100644 --- a/dist/package-shared/types/index.d.ts +++ b/dist/package-shared/types/index.d.ts @@ -1315,6 +1315,10 @@ export type DsqlCrudParam void; export interface MariaDBUser { Host?: string; @@ -2179,4 +2183,110 @@ export type CrudQueryObject

, "action" | "table">; }; +export type APIPathsParams = { + /** + * Full URL with http and query + * @example + * https://example.com/api/table?searchQuery={} + */ + href: string; + /** + * Base path before the dynamic path of the url. + * If no base path URL will be taken as the complete + * dynamic url + */ + basePath?: string; + auth?: () => Promise; + getMiddleware?: (params: { + query: APIPathsQuery; + }) => Promise>; + postMiddleware?: APIPathsParamsCrudMiddleware; + putMiddleware?: APIPathsParamsCrudMiddleware; + deleteMiddleware?: APIPathsParamsCrudMiddleware; + /** Runs For `POST`, `PUT`, and `DELETE` */ + crudMiddleware?: APIPathsParamsCrudMiddleware; + method: "GET" | "POST" | "PUT" | "DELETE"; + /** + * Request Query + */ + query?: APIPathsQuery; + /** + * Request Body + */ + body?: APIPathsBody; + allowedTables: APIPathsParamsAllowedTable[]; +}; +export type APIPathsBody = APIPathsQuery & { + data?: T; +}; +export type APIPathsQuery = { + searchQuery?: DsqlCrudQueryObject; + crudParams?: Pick, "count" | "countOnly" | "targetId" | "targetField" | "targetValue" | "tableSchema">; +}; +export type APIPathsParamsGetMiddleware = (params: { + query: APIPathsQuery; +}) => Promise>; +export type APIPathsParamsCrudMiddleware = (params: { + body: APIPathsBody; + query: DsqlCrudQueryObject; +}) => Promise>; +export type APIPathsParamsAllowedTable = { + table: string; + allowedFields?: (string | RegExp)[]; + disallowedFields?: (string | RegExp)[]; +}; +export type APIPathsCrudParams = APIPathsParams & { + isAuthorized?: boolean; + table: string; + targetId?: string; + allowedTable?: APIPathsParamsAllowedTable; + url?: URL; +}; +export type APIPathsData = { + table: string; + targetId?: string; + query?: APIPathsQuery; + url: URL; +}; +export type ClientCrudFetchParams = { + table: P; + method?: "GET" | "POST" | "PUT" | "DELETE"; + query?: APIPathsQuery; + body?: APIPathsBody; + basePath?: string; + targetId?: string | number | null; + apiOrigin?: string; +}; export {}; diff --git a/dist/package-shared/utils/data-fetching/crud-get.js b/dist/package-shared/utils/data-fetching/crud-get.js index f940d59..2e1d60e 100644 --- a/dist/package-shared/utils/data-fetching/crud-get.js +++ b/dist/package-shared/utils/data-fetching/crud-get.js @@ -83,11 +83,16 @@ function default_1(_a) { const parsedRes = (0, check_array_depth_1.default)(res, 2) ? (0, parseDbResults_1.default)({ unparsedResults: res[0], tableSchema }) : res[0]; - const parsedBatchRes = (0, check_array_depth_1.default)(res, 3) - ? res.map((_r) => { - return (0, parseDbResults_1.default)({ unparsedResults: _r[0], tableSchema }); - }) - : res; + const parsedBatchRes = connQueries.length > 1 + ? (0, check_array_depth_1.default)(res, 3) + ? res.map((_r) => { + return (0, parseDbResults_1.default)({ + unparsedResults: _r[0], + tableSchema, + }); + }) + : res + : undefined; const isSuccess = Array.isArray(res) && Array.isArray(res[0]); return { success: isSuccess, diff --git a/dist/package-shared/utils/data-fetching/crud.js b/dist/package-shared/utils/data-fetching/crud.js index c0ccd3e..6fe3be7 100644 --- a/dist/package-shared/utils/data-fetching/crud.js +++ b/dist/package-shared/utils/data-fetching/crud.js @@ -19,11 +19,27 @@ const crud_get_1 = __importDefault(require("./crud-get")); const conn_db_handler_1 = __importDefault(require("../db/conn-db-handler")); const addDbEntry_1 = __importDefault(require("../../functions/backend/db/addDbEntry")); const updateDbEntry_1 = __importDefault(require("../../functions/backend/db/updateDbEntry")); +const sql_generator_1 = __importDefault(require("../../functions/dsql/sql/sql-generator")); +const deleteDbEntry_1 = __importDefault(require("../../functions/backend/db/deleteDbEntry")); function dsqlCrud(params) { return __awaiter(this, void 0, void 0, function* () { - const { action, data, table, targetValue, sanitize, targetField, targetId, dbFullName, deleteData, batchData, deleteKeyValues, debug, tableSchema, deleteKeyValuesOperator, dbConfig, } = params; + const { action, data, table, targetValue, sanitize, targetField, targetId, dbFullName, deleteData, batchData, deleteKeyValues, debug, tableSchema, deleteKeyValuesOperator, dbConfig, query, } = params; const finalData = (sanitize ? sanitize({ data }) : data); const finalBatchData = (sanitize ? sanitize({ batchData }) : batchData); + const queryObject = query + ? (0, sql_generator_1.default)({ + tableName: table, + genObject: query, + dbFullName, + }) + : undefined; + const whereClause = queryObject === null || queryObject === void 0 ? void 0 : queryObject.string.replace(/^.*?( WHERE )/, "$1"); + const whereClauseObject = whereClause + ? { + clause: whereClause, + params: queryObject === null || queryObject === void 0 ? void 0 : queryObject.values.filter((v) => typeof v == "string" || typeof v == "number").map((v) => String(v)), + } + : undefined; switch (action) { case "get": return yield (0, crud_get_1.default)(params); @@ -51,33 +67,45 @@ function dsqlCrud(params) { debug, tableSchema, dbConfig, + whereClauseObject, }); return UPDATE_RESULT; case "delete": - const deleteQuery = (0, sql_delete_generator_1.default)({ - data: targetId - ? { id: targetId } - : targetField && targetValue - ? { [targetField]: targetValue } - : deleteData, - tableName: table, - dbFullName, - deleteKeyValues, - deleteKeyValuesOperator, - }); - const res = (yield (0, conn_db_handler_1.default)({ - query: deleteQuery === null || deleteQuery === void 0 ? void 0 : deleteQuery.query, - values: deleteQuery === null || deleteQuery === void 0 ? void 0 : deleteQuery.values, - dsqlConnOpts: { config: dbConfig }, - })); - return { - success: Boolean(res.affectedRows), - payload: res, - queryObject: { - sql: (0, sql_formatter_1.format)((deleteQuery === null || deleteQuery === void 0 ? void 0 : deleteQuery.query) || ""), - params: (deleteQuery === null || deleteQuery === void 0 ? void 0 : deleteQuery.values) || [], - }, - }; + let res; + if (whereClauseObject) { + const DELETE_RES = yield (0, deleteDbEntry_1.default)({ + whereClauseObject, + tableName: table, + dbFullName, + }); + return DELETE_RES; + } + else { + const deleteQuery = (0, sql_delete_generator_1.default)({ + data: targetId + ? { id: targetId } + : targetField && targetValue + ? { [targetField]: targetValue } + : deleteData, + tableName: table, + dbFullName, + deleteKeyValues, + deleteKeyValuesOperator, + }); + res = (yield (0, conn_db_handler_1.default)({ + query: deleteQuery === null || deleteQuery === void 0 ? void 0 : deleteQuery.query, + values: deleteQuery === null || deleteQuery === void 0 ? void 0 : deleteQuery.values, + dsqlConnOpts: { config: dbConfig }, + })); + return { + success: Boolean(res.affectedRows), + payload: res, + queryObject: { + sql: (0, sql_formatter_1.format)((deleteQuery === null || deleteQuery === void 0 ? void 0 : deleteQuery.query) || ""), + params: (deleteQuery === null || deleteQuery === void 0 ? void 0 : deleteQuery.values) || [], + }, + }; + } default: return { success: false, diff --git a/index.ts b/index.ts index fcfcd27..1c2c357 100644 --- a/index.ts +++ b/index.ts @@ -32,6 +32,7 @@ import grabDSQLConnection from "./package-shared/utils/grab-dsql-connection"; import grabDSQLConnectionConfig from "./package-shared/utils/grab-dsql-connection-config"; import schema from "./package-shared/api/schema"; import purgeDefaultFields from "./package-shared/utils/purge-default-fields"; +import apiCrudHandler from "./package-shared/api-paths"; /** * API Functions Object @@ -109,6 +110,7 @@ const datasquirel = { mail: { mailer: handleNodemailer, }, + apiCrudHandler, }; export default datasquirel; diff --git a/package-shared/api-paths/functions/delete-result.ts b/package-shared/api-paths/functions/delete-result.ts new file mode 100644 index 0000000..50cc183 --- /dev/null +++ b/package-shared/api-paths/functions/delete-result.ts @@ -0,0 +1,26 @@ +import { APIPathsCrudParams, APIResponseObject } from "../../types"; +import dsqlCrud from "../../utils/data-fetching/crud"; + +export default async function < + T extends { [k: string]: any } = { [k: string]: any } +>({ + table, + body, + targetId, +}: APIPathsCrudParams): Promise { + if (!targetId && !body?.searchQuery) { + throw new Error( + `Target ID or \`searchQuery\` field is required to delete Data.` + ); + } + + const DELETE_RESLT = await dsqlCrud({ + ...body?.crudParams, + action: "delete", + table, + query: body?.searchQuery, + targetId, + }); + + return DELETE_RESLT; +} diff --git a/package-shared/api-paths/functions/get-result.ts b/package-shared/api-paths/functions/get-result.ts new file mode 100644 index 0000000..fe94ae7 --- /dev/null +++ b/package-shared/api-paths/functions/get-result.ts @@ -0,0 +1,29 @@ +import { APIPathsCrudParams, APIResponseObject } from "../../types"; +import dsqlCrud from "../../utils/data-fetching/crud"; + +export default async function < + T extends { [k: string]: any } = { [k: string]: any } +>({ + table, + query, + allowedTable, + url, +}: APIPathsCrudParams): Promise { + if ( + (allowedTable?.allowedFields || allowedTable?.disallowedFields) && + !query?.searchQuery?.selectFields?.[0] + ) { + throw new Error( + `Please specify fields to select! Use the \`selectFields\` option in the query object` + ); + } + + const GET_RESULT = await dsqlCrud({ + ...query?.crudParams, + action: "get", + table, + query: query?.searchQuery, + }); + + return GET_RESULT; +} diff --git a/package-shared/api-paths/functions/post-result.ts b/package-shared/api-paths/functions/post-result.ts new file mode 100644 index 0000000..efa7d65 --- /dev/null +++ b/package-shared/api-paths/functions/post-result.ts @@ -0,0 +1,15 @@ +import { APIPathsCrudParams, APIResponseObject } from "../../types"; +import dsqlCrud from "../../utils/data-fetching/crud"; + +export default async function < + T extends { [k: string]: any } = { [k: string]: any } +>({ table, body }: APIPathsCrudParams): Promise { + const POST_RESULT = await dsqlCrud({ + ...body?.crudParams, + action: "insert", + table, + data: body?.data, + }); + + return POST_RESULT; +} diff --git a/package-shared/api-paths/functions/put-result.ts b/package-shared/api-paths/functions/put-result.ts new file mode 100644 index 0000000..71d0438 --- /dev/null +++ b/package-shared/api-paths/functions/put-result.ts @@ -0,0 +1,27 @@ +import { APIPathsCrudParams, APIResponseObject } from "../../types"; +import dsqlCrud from "../../utils/data-fetching/crud"; + +export default async function < + T extends { [k: string]: any } = { [k: string]: any } +>({ + table, + body, + targetId, +}: APIPathsCrudParams): Promise { + if (!targetId && !body?.searchQuery) { + throw new Error( + `Target ID or \`searchQuery\` field is required to update Data.` + ); + } + + const PUT_RESULT = await dsqlCrud({ + ...body?.crudParams, + action: "update", + table, + data: body?.data, + query: body?.searchQuery, + targetId, + }); + + return PUT_RESULT; +} diff --git a/package-shared/api-paths/index.ts b/package-shared/api-paths/index.ts new file mode 100644 index 0000000..acb65df --- /dev/null +++ b/package-shared/api-paths/index.ts @@ -0,0 +1,74 @@ +import _ from "lodash"; +import { + APIPathsCrudParams, + APIPathsParams, + APIResponseObject, +} from "../types"; +import getResult from "./functions/get-result"; +import { grabPathData } from "./utils/grab-path-data"; +import checks from "./utils/checks"; +import postResult from "./functions/post-result"; +import putResult from "./functions/put-result"; +import deleteResult from "./functions/delete-result"; + +export default async function apiCrudHandler< + T extends { [k: string]: any } = { [k: string]: any } +>(params: APIPathsParams): Promise { + try { + const { auth, method } = params; + + const isAuthorized = await auth?.(); + + const { table, targetId, url, query } = grabPathData(params); + + const crudParams: APIPathsCrudParams = { + ...params, + isAuthorized, + table, + targetId, + }; + + const checkedObj = await checks({ + ...crudParams, + query: _.merge(params.query || {}, query), + }); + + crudParams.query = checkedObj.query; + crudParams.body = checkedObj.body; + crudParams.allowedTable = checkedObj.allowedTable; + crudParams.url = url; + + if (targetId) { + if (crudParams.query) { + if (crudParams.query.crudParams) { + crudParams.query.crudParams.targetId = targetId; + } else { + crudParams.query.crudParams = { + targetId, + }; + } + } + } + + switch (method) { + case "GET": + return await getResult(crudParams); + case "POST": + return await postResult(crudParams); + case "PUT": + return await putResult(crudParams); + case "DELETE": + return await deleteResult(crudParams); + } + + return { + success: false, + msg: `Unhandled`, + }; + } catch (error: any) { + return { + success: false, + msg: error.message, + }; + } +} diff --git a/package-shared/api-paths/utils/checks.ts b/package-shared/api-paths/utils/checks.ts new file mode 100644 index 0000000..a20997a --- /dev/null +++ b/package-shared/api-paths/utils/checks.ts @@ -0,0 +1,166 @@ +import _ from "lodash"; +import { APIPathsCrudParams, APIPathsParamsAllowedTable } from "../../types"; + +export default async function checks< + T extends { [k: string]: any } = { [k: string]: any } +>({ + table, + allowedTables, + query, + body, + method, + getMiddleware, + postMiddleware, + putMiddleware, + deleteMiddleware, + crudMiddleware, +}: APIPathsCrudParams): Promise< + Pick, "query" | "body"> & { + allowedTable: APIPathsParamsAllowedTable; + } +> { + const allowedTable = allowedTables.find((tbl) => tbl.table == table); + + if (!allowedTable) { + throw new Error(`Can't Access this table: \`${table}\``); + } + + let newQuery = _.cloneDeep(query); + let newBody = _.cloneDeep(body); + + const searchFields = Object.keys(newQuery?.searchQuery?.query || {}); + const selectFields = ( + newQuery?.searchQuery?.selectFields + ? newQuery.searchQuery.selectFields.map((f) => + typeof f == "string" + ? f + : typeof f == "object" + ? f.fieldName + : undefined + ) + : undefined + )?.filter((f) => typeof f == "string"); + + const targetFields = [...(searchFields || []), ...(selectFields || [])]; + + if (method == "GET" && allowedTable.allowedFields) { + for (let i = 0; i < targetFields.length; i++) { + const fld = targetFields[i]; + const allowedFld = allowedTable.allowedFields.find((f) => + typeof f == "string" ? f == fld : fld.match(f) + ); + + if (!allowedFld) { + throw new Error(`\`${allowedFld}\` field not allowed`); + } + } + } + + if (method == "GET" && allowedTable.disallowedFields) { + for (let i = 0; i < targetFields.length; i++) { + const fld = targetFields[i]; + const disallowedFld = allowedTable.disallowedFields.find((f) => + typeof f == "string" ? f == fld : fld.match(f) + ); + + if (disallowedFld) { + throw new Error(`\`${disallowedFld}\` field not allowed`); + } + } + } + + if (method == "GET" && getMiddleware) { + newQuery = await getMiddleware({ query: newQuery || ({} as any) }); + } + + if (method !== "GET" && crudMiddleware) { + const middRes = await crudMiddleware({ + body: newBody || ({} as any), + query: newQuery || {}, + }); + + newBody = _.merge(newBody, middRes); + } + + if (method == "POST" && postMiddleware) { + const middRes = await postMiddleware({ + body: newBody || ({} as any), + query: newQuery || {}, + }); + + newBody = _.merge(newBody, middRes); + } + + if (method == "PUT" && putMiddleware) { + const middRes = await putMiddleware({ + body: newBody || ({} as any), + query: newQuery || {}, + }); + + newBody = _.merge(newBody, middRes); + } + + if (method == "DELETE" && deleteMiddleware) { + const middRes = await deleteMiddleware({ + body: newBody || ({} as any), + query: newQuery || {}, + }); + + newBody = _.merge(newBody, middRes); + } + + if (newQuery?.searchQuery?.join) { + for (let i = 0; i < newQuery.searchQuery.join.length; i++) { + const join = newQuery.searchQuery.join[i]; + const joinTableName = join.tableName; + const selectFields = join.selectFields; + + if (allowedTables?.[0]) { + const allowedJoinTable = allowedTables.find( + (t) => t.table == joinTableName + ); + + if (!allowedJoinTable?.table) { + throw new Error(`Can't joint \`${joinTableName}\` table`); + } + + const allowedFields = allowedJoinTable.allowedFields; + const disallowedFields = allowedJoinTable.disallowedFields; + + if (selectFields?.[0]) { + for (let j = 0; j < selectFields.length; j++) { + const selectField = selectFields[j]; + const selectFieldName = + typeof selectField == "object" + ? selectField.field + : String(selectField); + + if ( + allowedFields?.[0] && + !allowedFields.find( + (f) => String(f) == selectFieldName + ) + ) { + throw new Error(`Can't Select this Field!`); + } + + if ( + disallowedFields?.[0] && + disallowedFields.find( + (f) => String(f) == selectFieldName + ) + ) { + throw new Error(`Disallowed Field Selected!`); + } + } + } + } + } + } + + return { + query: newQuery, + body: newBody, + allowedTable, + }; +} diff --git a/package-shared/api-paths/utils/grab-path-data.ts b/package-shared/api-paths/utils/grab-path-data.ts new file mode 100644 index 0000000..152f848 --- /dev/null +++ b/package-shared/api-paths/utils/grab-path-data.ts @@ -0,0 +1,34 @@ +import { APIPathsData, APIPathsParams, APIPathsQuery } from "../../types"; +import deserializeQuery from "../../utils/deserialize-query"; + +export function grabPathData< + T extends { [k: string]: any } = { [k: string]: any } +>({ href, basePath }: APIPathsParams): APIPathsData { + const urlObj = new URL(href); + + const pathname = basePath + ? urlObj.pathname.replace(basePath, "") + : urlObj.pathname; + + const urlArray = pathname.split("/").filter((u) => Boolean(u.match(/./))); + + const table = urlArray[0]; + const targetId = urlArray[1]; + + let query = ( + urlObj?.searchParams + ? deserializeQuery(Object.fromEntries(urlObj.searchParams)) + : undefined + ) as APIPathsQuery | undefined; + + if (!table) { + throw new Error(`No Table Found`); + } + + return { + table, + targetId, + query, + url: urlObj, + }; +} diff --git a/package-shared/functions/backend/db/deleteDbEntry.ts b/package-shared/functions/backend/db/deleteDbEntry.ts index 04a115b..ef67366 100644 --- a/package-shared/functions/backend/db/deleteDbEntry.ts +++ b/package-shared/functions/backend/db/deleteDbEntry.ts @@ -1,4 +1,10 @@ -import { DSQL_TableSchemaType, PostInsertReturn } from "../../../types"; +import { format } from "sql-formatter"; +import { + APIResponseObject, + DSQL_TableSchemaType, + DsqlCrudParamWhereClause, + PostInsertReturn, +} from "../../../types"; import checkIfIsMaster from "../../../utils/check-if-is-master"; import connDbHandler from "../../../utils/db/conn-db-handler"; import { DbContextsArray } from "./runQuery"; @@ -8,9 +14,10 @@ type Param = { dbFullName?: string; tableName: K; tableSchema?: DSQL_TableSchemaType; - identifierColumnName: keyof T; - identifierValue: string | number; + identifierColumnName?: keyof T; + identifierValue?: string | number; forceLocal?: boolean; + whereClauseObject?: DsqlCrudParamWhereClause; }; /** @@ -27,7 +34,8 @@ export default async function deleteDbEntry< identifierColumnName, identifierValue, forceLocal, -}: Param): Promise { + whereClauseObject, +}: Param): Promise> { try { const isMaster = forceLocal ? true @@ -38,21 +46,46 @@ export default async function deleteDbEntry< * * @description */ - const query = `DELETE FROM ${ + let query = `DELETE FROM ${ isMaster && !dbFullName ? "" : `\`${dbFullName}\`.` - }\`${tableName}\` WHERE \`${identifierColumnName.toString()}\`=?`; + }\`${tableName}\``; - const deletedEntry = await connDbHandler({ + let values: any[] = []; + + if (whereClauseObject) { + query += ` ${whereClauseObject.clause}`; + values.push(...(whereClauseObject.params || [])); + } else if (identifierColumnName && identifierValue) { + query += ` WHERE \`${identifierColumnName.toString()}\`=?`; + values = [identifierValue]; + } else { + throw new Error( + `Delete operation has no specified rows! Can't delete everything in this table!` + ); + } + + const deletedEntry = (await connDbHandler({ query, - values: [identifierValue], - }); + values, + })) as PostInsertReturn; /** * Return statement */ - return deletedEntry; + return { + success: Boolean(deletedEntry.affectedRows), + payload: deletedEntry, + queryObject: { + sql: format(query), + params: values, + }, + }; } catch (error: any) { - console.log("Error Deleting Entry =>", error.message); - return null; + const errorMsg = `Error Deleting Entry =>, ${error.message}`; + console.log(errorMsg); + return { + success: false, + msg: errorMsg, + }; } } diff --git a/package-shared/functions/backend/db/grab-parsed-value.ts b/package-shared/functions/backend/db/grab-parsed-value.ts index ddcc5fd..b1ecade 100644 --- a/package-shared/functions/backend/db/grab-parsed-value.ts +++ b/package-shared/functions/backend/db/grab-parsed-value.ts @@ -33,6 +33,7 @@ export default function grabParsedValue({ if (typeof newValue == "undefined") { return; } + if ( typeof newValue !== "string" && typeof newValue !== "number" && @@ -94,7 +95,7 @@ export default function grabParsedValue({ typeof newValue === "string" && (newValue.match(/^null$/i) || !newValue.match(/./i)) ) { - newValue = undefined; + newValue = ""; } if ( diff --git a/package-shared/functions/backend/db/updateDbEntry.ts b/package-shared/functions/backend/db/updateDbEntry.ts index 358b374..d93c71e 100644 --- a/package-shared/functions/backend/db/updateDbEntry.ts +++ b/package-shared/functions/backend/db/updateDbEntry.ts @@ -5,6 +5,7 @@ import { DbContextsArray } from "./runQuery"; import { APIResponseObject, DSQL_TableSchemaType, + DsqlCrudParamWhereClause, PostInsertReturn, } from "../../../types"; import _ from "lodash"; @@ -25,6 +26,7 @@ type Param = { forceLocal?: boolean; debug?: boolean; dbConfig?: ConnectionConfig; + whereClauseObject?: DsqlCrudParamWhereClause; }; /** @@ -46,6 +48,7 @@ export default async function updateDbEntry< forceLocal, debug, dbConfig, + whereClauseObject, }: Param): Promise> { /** * Check if data is valid @@ -135,13 +138,17 @@ export default async function updateDbEntry< //////////////////////////////////////// //////////////////////////////////////// - const query = `UPDATE ${ + let query = `UPDATE ${ isMaster && !dbFullName ? "" : `\`${dbFullName}\`.` - }\`${tableName}\` SET ${updateKeyValueArray.join(",")} WHERE \`${ - identifierColumnName as string - }\`=?`; + }\`${tableName}\` SET ${updateKeyValueArray.join(",")}`; - updateValues.push(identifierValue); + if (whereClauseObject) { + query += ` ${whereClauseObject.clause}`; + updateValues.push(...(whereClauseObject.params || [])); + } else { + query += ` WHERE \`${identifierColumnName as string}\`=?`; + updateValues.push(identifierValue); + } const updatedEntry = await connDbHandler({ query, diff --git a/package-shared/types/index.ts b/package-shared/types/index.ts index bec4db7..c75a73d 100644 --- a/package-shared/types/index.ts +++ b/package-shared/types/index.ts @@ -1543,6 +1543,11 @@ export type DsqlCrudParam< dbConfig?: ConnectionConfig; }; +export type DsqlCrudParamWhereClause = { + clause: string; + params?: string[]; +}; + export type ErrorCallback = (title: string, error: Error, data?: any) => void; export interface MariaDBUser { @@ -2840,3 +2845,109 @@ export type CrudQueryObject< userKey?: string; crudParams?: Omit, "action" | "table">; }; + +export type APIPathsParams< + T extends { [k: string]: any } = { [k: string]: any } +> = { + /** + * Full URL with http and query + * @example + * https://example.com/api/table?searchQuery={} + */ + href: string; + /** + * Base path before the dynamic path of the url. + * If no base path URL will be taken as the complete + * dynamic url + */ + basePath?: string; + auth?: () => Promise; + getMiddleware?: (params: { + query: APIPathsQuery; + }) => Promise>; + postMiddleware?: APIPathsParamsCrudMiddleware; + putMiddleware?: APIPathsParamsCrudMiddleware; + deleteMiddleware?: APIPathsParamsCrudMiddleware; + /** Runs For `POST`, `PUT`, and `DELETE` */ + crudMiddleware?: APIPathsParamsCrudMiddleware; + method: "GET" | "POST" | "PUT" | "DELETE"; + /** + * Request Query + */ + query?: APIPathsQuery; + /** + * Request Body + */ + body?: APIPathsBody; + allowedTables: APIPathsParamsAllowedTable[]; +}; + +export type APIPathsBody< + T extends { [k: string]: any } = { [k: string]: any } +> = APIPathsQuery & { + data?: T; +}; + +export type APIPathsQuery< + T extends { [k: string]: any } = { [k: string]: any } +> = { + searchQuery?: DsqlCrudQueryObject; + crudParams?: Pick< + DsqlCrudParam, + | "count" + | "countOnly" + | "targetId" + | "targetField" + | "targetValue" + | "tableSchema" + >; +}; + +export type APIPathsParamsGetMiddleware< + T extends { [k: string]: any } = { [k: string]: any } +> = (params: { query: APIPathsQuery }) => Promise>; + +export type APIPathsParamsCrudMiddleware< + T extends { [k: string]: any } = { [k: string]: any } +> = (params: { + body: APIPathsBody; + query: DsqlCrudQueryObject; +}) => Promise>; + +export type APIPathsParamsAllowedTable = { + table: string; + allowedFields?: (string | RegExp)[]; + disallowedFields?: (string | RegExp)[]; +}; + +export type APIPathsCrudParams< + T extends { [k: string]: any } = { [k: string]: any } +> = APIPathsParams & { + isAuthorized?: boolean; + table: string; + targetId?: string; + allowedTable?: APIPathsParamsAllowedTable; + url?: URL; +}; + +export type APIPathsData< + T extends { [k: string]: any } = { [k: string]: any } +> = { + table: string; + targetId?: string; + query?: APIPathsQuery; + url: URL; +}; + +export type ClientCrudFetchParams< + T extends { [k: string]: any } = { [k: string]: any }, + P = string +> = { + table: P; + method?: "GET" | "POST" | "PUT" | "DELETE"; + query?: APIPathsQuery; + body?: APIPathsBody; + basePath?: string; + targetId?: string | number | null; + apiOrigin?: string; +}; diff --git a/package-shared/utils/data-fetching/crud-get.ts b/package-shared/utils/data-fetching/crud-get.ts index 6d45986..6bf23b7 100644 --- a/package-shared/utils/data-fetching/crud-get.ts +++ b/package-shared/utils/data-fetching/crud-get.ts @@ -107,11 +107,17 @@ export default async function < const parsedRes = checkArrayDepth(res, 2) ? parseDbResults({ unparsedResults: res[0], tableSchema }) : res[0]; - const parsedBatchRes = checkArrayDepth(res, 3) - ? res.map((_r: any[][]) => { - return parseDbResults({ unparsedResults: _r[0], tableSchema }); - }) - : res; + const parsedBatchRes = + connQueries.length > 1 + ? checkArrayDepth(res, 3) + ? res.map((_r: any[][]) => { + return parseDbResults({ + unparsedResults: _r[0], + tableSchema, + }); + }) + : res + : undefined; const isSuccess = Array.isArray(res) && Array.isArray(res[0]); diff --git a/package-shared/utils/data-fetching/crud.ts b/package-shared/utils/data-fetching/crud.ts index da9492e..7875602 100644 --- a/package-shared/utils/data-fetching/crud.ts +++ b/package-shared/utils/data-fetching/crud.ts @@ -9,6 +9,8 @@ import dsqlCrudGet from "./crud-get"; import connDbHandler from "../db/conn-db-handler"; import addDbEntry from "../../functions/backend/db/addDbEntry"; import updateDbEntry from "../../functions/backend/db/updateDbEntry"; +import sqlGenerator from "../../functions/dsql/sql/sql-generator"; +import deleteDbEntry from "../../functions/backend/db/deleteDbEntry"; export default async function dsqlCrud< T extends { [key: string]: any } = { [key: string]: any }, @@ -30,6 +32,7 @@ export default async function dsqlCrud< tableSchema, deleteKeyValuesOperator, dbConfig, + query, } = params; const finalData = (sanitize ? sanitize({ data }) : data) as T; @@ -37,6 +40,25 @@ export default async function dsqlCrud< sanitize ? sanitize({ batchData }) : batchData ) as T[]; + const queryObject = query + ? sqlGenerator({ + tableName: table, + genObject: query, + dbFullName, + }) + : undefined; + + const whereClause = queryObject?.string.replace(/^.*?( WHERE )/, "$1"); + + const whereClauseObject = whereClause + ? { + clause: whereClause!, + params: queryObject?.values + .filter((v) => typeof v == "string" || typeof v == "number") + .map((v) => String(v)), + } + : undefined; + switch (action) { case "get": return await dsqlCrudGet(params); @@ -68,37 +90,50 @@ export default async function dsqlCrud< debug, tableSchema, dbConfig, + whereClauseObject, }); return UPDATE_RESULT; case "delete": - const deleteQuery = sqlDeleteGenerator({ - data: targetId - ? { id: targetId } - : targetField && targetValue - ? { [targetField]: targetValue } - : deleteData, - tableName: table, - dbFullName, - deleteKeyValues, - deleteKeyValuesOperator, - }); + let res: PostInsertReturn; - const res = (await connDbHandler({ - query: deleteQuery?.query, - values: deleteQuery?.values, - dsqlConnOpts: { config: dbConfig }, - })) as PostInsertReturn; + if (whereClauseObject) { + const DELETE_RES = await deleteDbEntry({ + whereClauseObject, + tableName: table, + dbFullName, + }); - return { - success: Boolean(res.affectedRows), - payload: res, - queryObject: { - sql: format(deleteQuery?.query || ""), - params: deleteQuery?.values || [], - }, - }; + return DELETE_RES; + } else { + const deleteQuery = sqlDeleteGenerator({ + data: targetId + ? { id: targetId } + : targetField && targetValue + ? { [targetField]: targetValue } + : deleteData, + tableName: table, + dbFullName, + deleteKeyValues, + deleteKeyValuesOperator, + }); + + res = (await connDbHandler({ + query: deleteQuery?.query, + values: deleteQuery?.values, + dsqlConnOpts: { config: dbConfig }, + })) as PostInsertReturn; + + return { + success: Boolean(res.affectedRows), + payload: res, + queryObject: { + sql: format(deleteQuery?.query || ""), + params: deleteQuery?.values || [], + }, + }; + } default: return { diff --git a/package.json b/package.json index 330dd38..2828413 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@moduletrace/datasquirel", - "version": "5.5.5", + "version": "5.5.6", "description": "Cloud-based SQL data management tool", "main": "dist/index.js", "bin": { @@ -48,5 +48,8 @@ "nodemailer": "^6.9.14", "sanitize-html": "^2.13.1", "sql-formatter": "^15.6.10" + }, + "devDependencies": { + "@types/bun": "^1.3.5" } }