@ruiapp/rapid-core 0.1.81 → 0.1.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist/index.js +69 -27
  2. package/package.json +1 -1
  3. package/rollup.config.js +16 -16
  4. package/src/core/actionHandler.ts +22 -22
  5. package/src/core/eventManager.ts +20 -20
  6. package/src/core/facility.ts +7 -7
  7. package/src/core/http/formDataParser.ts +89 -89
  8. package/src/core/pluginManager.ts +175 -175
  9. package/src/core/providers/runtimeProvider.ts +5 -5
  10. package/src/core/request.ts +86 -86
  11. package/src/core/response.ts +76 -76
  12. package/src/core/routeContext.ts +43 -43
  13. package/src/core/routesBuilder.ts +88 -88
  14. package/src/dataAccess/dataAccessor.ts +137 -137
  15. package/src/dataAccess/entityManager.ts +74 -26
  16. package/src/deno-std/datetime/to_imf.ts +32 -32
  17. package/src/deno-std/encoding/base64.ts +141 -141
  18. package/src/facilities/log/LogFacility.ts +35 -35
  19. package/src/helpers/entityHelpers.ts +76 -76
  20. package/src/plugins/auth/actionHandlers/changePassword.ts +54 -54
  21. package/src/plugins/auth/actionHandlers/createSession.ts +63 -63
  22. package/src/plugins/auth/actionHandlers/deleteSession.ts +18 -18
  23. package/src/plugins/auth/actionHandlers/getMyProfile.ts +35 -35
  24. package/src/plugins/auth/actionHandlers/index.ts +8 -8
  25. package/src/plugins/auth/actionHandlers/resetPassword.ts +38 -38
  26. package/src/plugins/auth/models/AccessToken.ts +56 -56
  27. package/src/plugins/auth/models/index.ts +3 -3
  28. package/src/plugins/auth/routes/changePassword.ts +15 -15
  29. package/src/plugins/auth/routes/getMyProfile.ts +15 -15
  30. package/src/plugins/auth/routes/index.ts +7 -7
  31. package/src/plugins/auth/routes/resetPassword.ts +15 -15
  32. package/src/plugins/auth/routes/signin.ts +15 -15
  33. package/src/plugins/auth/routes/signout.ts +15 -15
  34. package/src/plugins/cronJob/CronJobPluginTypes.ts +49 -49
  35. package/src/plugins/cronJob/actionHandlers/index.ts +4 -4
  36. package/src/plugins/cronJob/actionHandlers/runCronJob.ts +29 -29
  37. package/src/plugins/cronJob/routes/index.ts +3 -3
  38. package/src/plugins/cronJob/routes/runCronJob.ts +15 -15
  39. package/src/plugins/dataManage/actionHandlers/addEntityRelations.ts +20 -20
  40. package/src/plugins/dataManage/actionHandlers/countCollectionEntities.ts +15 -15
  41. package/src/plugins/dataManage/actionHandlers/createCollectionEntitiesBatch.ts +42 -42
  42. package/src/plugins/dataManage/actionHandlers/createCollectionEntity.ts +24 -24
  43. package/src/plugins/dataManage/actionHandlers/findCollectionEntities.ts +26 -26
  44. package/src/plugins/dataManage/actionHandlers/findCollectionEntityById.ts +21 -21
  45. package/src/plugins/dataManage/actionHandlers/queryDatabase.ts +22 -22
  46. package/src/plugins/dataManage/actionHandlers/removeEntityRelations.ts +20 -20
  47. package/src/plugins/dataManage/actionHandlers/updateCollectionEntityById.ts +35 -35
  48. package/src/plugins/fileManage/actionHandlers/downloadDocument.ts +36 -36
  49. package/src/plugins/fileManage/actionHandlers/uploadFile.ts +33 -33
  50. package/src/plugins/fileManage/routes/downloadDocument.ts +15 -15
  51. package/src/plugins/fileManage/routes/downloadFile.ts +15 -15
  52. package/src/plugins/fileManage/routes/index.ts +5 -5
  53. package/src/plugins/fileManage/routes/uploadFile.ts +15 -15
  54. package/src/plugins/metaManage/actionHandlers/getMetaModelDetail.ts +10 -10
  55. package/src/plugins/metaManage/actionHandlers/listMetaModels.ts +9 -9
  56. package/src/plugins/metaManage/actionHandlers/listMetaRoutes.ts +9 -9
  57. package/src/plugins/routeManage/actionHandlers/httpProxy.ts +13 -13
  58. package/src/plugins/sequence/SequenceService.ts +81 -81
  59. package/src/plugins/sequence/actionHandlers/generateSn.ts +32 -32
  60. package/src/plugins/sequence/actionHandlers/index.ts +4 -4
  61. package/src/plugins/sequence/models/SequenceAutoIncrementRecord.ts +49 -49
  62. package/src/plugins/sequence/models/SequenceRule.ts +42 -42
  63. package/src/plugins/sequence/models/index.ts +4 -4
  64. package/src/plugins/sequence/routes/generateSn.ts +15 -15
  65. package/src/plugins/sequence/routes/index.ts +3 -3
  66. package/src/plugins/sequence/segment-utility.ts +11 -11
  67. package/src/plugins/sequence/segments/index.ts +9 -9
  68. package/src/plugins/serverOperation/ServerOperationPlugin.ts +91 -91
  69. package/src/plugins/serverOperation/ServerOperationPluginTypes.ts +15 -15
  70. package/src/plugins/serverOperation/actionHandlers/index.ts +4 -4
  71. package/src/plugins/setting/SettingService.ts +213 -213
  72. package/src/plugins/setting/actionHandlers/getSystemSettingValues.ts +30 -30
  73. package/src/plugins/setting/actionHandlers/getUserSettingValues.ts +38 -38
  74. package/src/plugins/setting/actionHandlers/index.ts +6 -6
  75. package/src/plugins/setting/actionHandlers/setSystemSettingValues.ts +30 -30
  76. package/src/plugins/setting/models/SystemSettingGroupSetting.ts +57 -57
  77. package/src/plugins/setting/models/SystemSettingItem.ts +42 -42
  78. package/src/plugins/setting/models/SystemSettingItemSetting.ts +73 -73
  79. package/src/plugins/setting/models/UserSettingGroupSetting.ts +57 -57
  80. package/src/plugins/setting/models/UserSettingItem.ts +49 -49
  81. package/src/plugins/setting/models/UserSettingItemSetting.ts +73 -73
  82. package/src/plugins/setting/models/index.ts +8 -8
  83. package/src/plugins/setting/routes/getSystemSettingValues.ts +15 -15
  84. package/src/plugins/setting/routes/getUserSettingValues.ts +15 -15
  85. package/src/plugins/setting/routes/index.ts +5 -5
  86. package/src/plugins/setting/routes/setSystemSettingValues.ts +15 -15
  87. package/src/plugins/stateMachine/actionHandlers/index.ts +4 -4
  88. package/src/plugins/stateMachine/actionHandlers/sendStateMachineEvent.ts +51 -51
  89. package/src/plugins/stateMachine/models/StateMachine.ts +42 -42
  90. package/src/plugins/stateMachine/models/index.ts +3 -3
  91. package/src/plugins/stateMachine/routes/index.ts +3 -3
  92. package/src/plugins/stateMachine/routes/sendStateMachineEvent.ts +15 -15
  93. package/src/polyfill.ts +5 -5
  94. package/src/proxy/mod.ts +38 -38
  95. package/src/utilities/accessControlUtility.ts +33 -33
  96. package/src/utilities/fsUtility.ts +61 -61
  97. package/src/utilities/httpUtility.ts +19 -19
  98. package/src/utilities/jwtUtility.ts +26 -26
  99. package/src/utilities/timeUtility.ts +9 -9
  100. package/tsconfig.json +19 -19
package/dist/index.js CHANGED
@@ -2262,6 +2262,9 @@ function convertEntityOrderByToRowOrderBy(server, model, baseModel, orderByList)
2262
2262
  }
2263
2263
  if (relationField) {
2264
2264
  const relationProperty = getEntityPropertyByCode(server, model, relationField);
2265
+ if (!relationProperty) {
2266
+ throw new Error(`Property '${relationProperty}' was not found in ${model.namespace}.${model.singularCode}`);
2267
+ }
2265
2268
  if (!isRelationProperty(relationProperty)) {
2266
2269
  throw new Error("orderBy[].relation must be a one-relation property.");
2267
2270
  }
@@ -2318,7 +2321,12 @@ async function findEntities(server, dataAccessor, options) {
2318
2321
  let relationOptions = options.relations || {};
2319
2322
  let relationPropertyCodes = Object.keys(relationOptions) || [];
2320
2323
  if (!options.properties || !options.properties.length) {
2321
- propertiesToSelect = getEntityPropertiesIncludingBase(server, model).filter((property) => !isRelationProperty(property) || relationPropertyCodes.includes(property.code));
2324
+ propertiesToSelect = getEntityPropertiesIncludingBase(server, model).filter((property) => {
2325
+ if (!property) {
2326
+ throw new Error(`Property '${property}' was not found in ${model.namespace}.${model.singularCode}`);
2327
+ }
2328
+ return !options.keepNonPropertyFields || isRelationProperty(property) || relationPropertyCodes.includes(property.code);
2329
+ });
2322
2330
  }
2323
2331
  else {
2324
2332
  propertiesToSelect = getEntityPropertiesIncludingBase(server, model).filter((property) => options.properties.includes(property.code) || relationPropertyCodes.includes(property.code));
@@ -2326,6 +2334,9 @@ async function findEntities(server, dataAccessor, options) {
2326
2334
  const columnsToSelect = [];
2327
2335
  const relationPropertiesToSelect = [];
2328
2336
  lodash.forEach(propertiesToSelect, (property) => {
2337
+ if (!property) {
2338
+ throw new Error(`Property '${property}' was not found in ${model.namespace}.${model.singularCode}`);
2339
+ }
2329
2340
  if (isRelationProperty(property)) {
2330
2341
  relationPropertiesToSelect.push(property);
2331
2342
  if (property.relation === "one" && !property.linkTableName) {
@@ -2864,8 +2875,7 @@ async function createEntity(server, dataAccessor, options, plugin) {
2864
2875
  lodash.keys(entity).forEach((propertyCode) => {
2865
2876
  const property = getEntityPropertyByCode(server, model, propertyCode);
2866
2877
  if (!property) {
2867
- // Unknown property
2868
- return;
2878
+ throw new Error(`Property '${property}' was not found in ${model.namespace}.${model.singularCode}`);
2869
2879
  }
2870
2880
  if (isRelationProperty(property)) {
2871
2881
  if (property.relation === "many") {
@@ -3097,8 +3107,7 @@ async function updateEntityById(server, dataAccessor, options, plugin) {
3097
3107
  lodash.keys(changes).forEach((propertyCode) => {
3098
3108
  const property = getEntityPropertyByCode(server, model, propertyCode);
3099
3109
  if (!property) {
3100
- // Unknown property
3101
- return;
3110
+ throw new Error(`Property '${property}' was not found in ${model.namespace}.${model.singularCode}`);
3102
3111
  }
3103
3112
  if (isRelationProperty(property)) {
3104
3113
  if (property.relation === "many") {
@@ -3181,12 +3190,41 @@ async function updateEntityById(server, dataAccessor, options, plugin) {
3181
3190
  if (!lodash.isArray(relatedEntitiesToBeSaved)) {
3182
3191
  throw new Error(`Value of field '${property.code}' should be an array.`);
3183
3192
  }
3193
+ const targetIdsToKeep = [];
3194
+ for (const relatedEntityToBeSaved of relatedEntitiesToBeSaved) {
3195
+ let relatedEntityId;
3196
+ if (lodash.isObject(relatedEntityToBeSaved)) {
3197
+ relatedEntityId = relatedEntityToBeSaved["id"];
3198
+ }
3199
+ else {
3200
+ relatedEntityId = relatedEntityToBeSaved;
3201
+ }
3202
+ if (relatedEntityId) {
3203
+ targetIdsToKeep.push(relatedEntityId);
3204
+ }
3205
+ }
3206
+ let currentTargetIds = [];
3184
3207
  if (property.linkTableName) {
3185
- // TODO: should support removing relation
3208
+ const targetLinks = await server.queryDatabaseObject(`SELECT ${server.queryBuilder.quoteObject(property.targetIdColumnName)} FROM ${server.queryBuilder.quoteTable({
3209
+ schema: property.linkSchema,
3210
+ tableName: property.linkTableName,
3211
+ })} WHERE ${server.queryBuilder.quoteObject(property.selfIdColumnName)} = $1`, [id]);
3212
+ currentTargetIds = targetLinks.map((item) => item[property.targetIdColumnName]);
3186
3213
  await server.queryDatabaseObject(`DELETE FROM ${server.queryBuilder.quoteTable({
3187
3214
  schema: property.linkSchema,
3188
3215
  tableName: property.linkTableName,
3216
+ })} WHERE ${server.queryBuilder.quoteObject(property.selfIdColumnName)} = $1
3217
+ AND ${server.queryBuilder.quoteObject(property.targetIdColumnName)} <> ALL($2::int[])`, [id, targetIdsToKeep]);
3218
+ }
3219
+ else {
3220
+ const targetModel = server.getModel({
3221
+ singularCode: property.targetSingularCode,
3222
+ });
3223
+ const targetRows = await server.queryDatabaseObject(`SELECT id FROM ${server.queryBuilder.quoteTable({
3224
+ schema: targetModel.schema,
3225
+ tableName: targetModel.tableName,
3189
3226
  })} WHERE ${server.queryBuilder.quoteObject(property.selfIdColumnName)} = $1`, [id]);
3227
+ currentTargetIds = targetRows.map((item) => item.id);
3190
3228
  }
3191
3229
  for (const relatedEntityToBeSaved of relatedEntitiesToBeSaved) {
3192
3230
  let relatedEntityId;
@@ -3218,6 +3256,31 @@ async function updateEntityById(server, dataAccessor, options, plugin) {
3218
3256
  if (!targetEntity) {
3219
3257
  throw new Error(`Entity with id '${relatedEntityId}' in field '${property.code}' is not exists.`);
3220
3258
  }
3259
+ if (!currentTargetIds.includes(relatedEntityId)) {
3260
+ if (property.linkTableName) {
3261
+ const command = `INSERT INTO ${server.queryBuilder.quoteTable({
3262
+ schema: property.linkSchema,
3263
+ tableName: property.linkTableName,
3264
+ })} (${server.queryBuilder.quoteObject(property.selfIdColumnName)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
3265
+ const params = [id, relatedEntityId];
3266
+ await server.queryDatabaseObject(command, params);
3267
+ }
3268
+ else {
3269
+ await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName]: id });
3270
+ targetEntity[property.selfIdColumnName] = id;
3271
+ }
3272
+ }
3273
+ relatedEntities.push(targetEntity);
3274
+ }
3275
+ }
3276
+ else {
3277
+ // fieldValue is id
3278
+ relatedEntityId = relatedEntityToBeSaved;
3279
+ const targetEntity = await targetDataAccessor.findById(relatedEntityId);
3280
+ if (!targetEntity) {
3281
+ throw new Error(`Entity with id '${relatedEntityId}' in field '${property.code}' is not exists.`);
3282
+ }
3283
+ if (!currentTargetIds.includes(relatedEntityId)) {
3221
3284
  if (property.linkTableName) {
3222
3285
  const command = `INSERT INTO ${server.queryBuilder.quoteTable({
3223
3286
  schema: property.linkSchema,
@@ -3230,27 +3293,6 @@ async function updateEntityById(server, dataAccessor, options, plugin) {
3230
3293
  await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName]: id });
3231
3294
  targetEntity[property.selfIdColumnName] = id;
3232
3295
  }
3233
- relatedEntities.push(targetEntity);
3234
- }
3235
- }
3236
- else {
3237
- // fieldValue is id
3238
- relatedEntityId = relatedEntityToBeSaved;
3239
- const targetEntity = await targetDataAccessor.findById(relatedEntityId);
3240
- if (!targetEntity) {
3241
- throw new Error(`Entity with id '${relatedEntityId}' in field '${property.code}' is not exists.`);
3242
- }
3243
- if (property.linkTableName) {
3244
- const command = `INSERT INTO ${server.queryBuilder.quoteTable({
3245
- schema: property.linkSchema,
3246
- tableName: property.linkTableName,
3247
- })} (${server.queryBuilder.quoteObject(property.selfIdColumnName)}, ${property.targetIdColumnName}) VALUES ($1, $2) ON CONFLICT DO NOTHING;`;
3248
- const params = [id, relatedEntityId];
3249
- await server.queryDatabaseObject(command, params);
3250
- }
3251
- else {
3252
- await targetDataAccessor.updateById(targetEntity.id, { [property.selfIdColumnName]: id });
3253
- targetEntity[property.selfIdColumnName] = id;
3254
3296
  }
3255
3297
  relatedEntities.push(targetEntity);
3256
3298
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ruiapp/rapid-core",
3
- "version": "0.1.81",
3
+ "version": "0.1.83",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/rollup.config.js CHANGED
@@ -1,16 +1,16 @@
1
- import typescript from "rollup-plugin-typescript2";
2
- import tscAlias from "rollup-plugin-tsc-alias";
3
-
4
- export default {
5
- input: ["src/index.ts"],
6
- output: [
7
- {
8
- dir: "dist",
9
- entryFileNames: "[name].js",
10
- format: "cjs",
11
- exports: "named",
12
- },
13
- ],
14
- plugins: [typescript(), tscAlias()],
15
- external: [],
16
- };
1
+ import typescript from "rollup-plugin-typescript2";
2
+ import tscAlias from "rollup-plugin-tsc-alias";
3
+
4
+ export default {
5
+ input: ["src/index.ts"],
6
+ output: [
7
+ {
8
+ dir: "dist",
9
+ entryFileNames: "[name].js",
10
+ format: "cjs",
11
+ exports: "named",
12
+ },
13
+ ],
14
+ plugins: [typescript(), tscAlias()],
15
+ external: [],
16
+ };
@@ -1,22 +1,22 @@
1
- import { RpdApplicationConfig } from "~/types";
2
- import { IRpdServer, RapidPlugin } from "./server";
3
- import { Next, RouteContext } from "./routeContext";
4
- import { Logger } from "~/facilities/log/LogFacility";
5
-
6
- export interface ActionHandlerContext {
7
- logger: Logger;
8
- routerContext: RouteContext;
9
- next: Next;
10
- server: IRpdServer;
11
- applicationConfig: RpdApplicationConfig;
12
- input?: any;
13
- output?: any;
14
- status?: Response["status"];
15
- }
16
-
17
- export type ActionHandler = (ctx: ActionHandlerContext, options: any) => void | Promise<void>;
18
-
19
- export interface IPluginActionHandler {
20
- code: string;
21
- handler: (plugin: RapidPlugin, ctx: ActionHandlerContext, options: any) => void | Promise<void>;
22
- }
1
+ import { RpdApplicationConfig } from "~/types";
2
+ import { IRpdServer, RapidPlugin } from "./server";
3
+ import { Next, RouteContext } from "./routeContext";
4
+ import { Logger } from "~/facilities/log/LogFacility";
5
+
6
+ export interface ActionHandlerContext {
7
+ logger: Logger;
8
+ routerContext: RouteContext;
9
+ next: Next;
10
+ server: IRpdServer;
11
+ applicationConfig: RpdApplicationConfig;
12
+ input?: any;
13
+ output?: any;
14
+ status?: Response["status"];
15
+ }
16
+
17
+ export type ActionHandler = (ctx: ActionHandlerContext, options: any) => void | Promise<void>;
18
+
19
+ export interface IPluginActionHandler {
20
+ code: string;
21
+ handler: (plugin: RapidPlugin, ctx: ActionHandlerContext, options: any) => void | Promise<void>;
22
+ }
@@ -1,20 +1,20 @@
1
- import { EventEmitter } from "events";
2
-
3
- export default class EventManager<EventTypes extends Record<string, any[]>> {
4
- #eventEmitter: EventEmitter;
5
-
6
- constructor() {
7
- this.#eventEmitter = new EventEmitter();
8
- }
9
-
10
- on<K extends keyof EventTypes>(eventName: K, listener: (...args: EventTypes[K]) => void) {
11
- this.#eventEmitter.on(eventName as string, listener);
12
- }
13
-
14
- async emit<K extends keyof EventTypes>(eventName: K, ...args: EventTypes[K]) {
15
- const listeners = this.#eventEmitter.listeners(eventName as string);
16
- for (const listener of listeners) {
17
- await listener(...args);
18
- }
19
- }
20
- }
1
+ import { EventEmitter } from "events";
2
+
3
+ export default class EventManager<EventTypes extends Record<string, any[]>> {
4
+ #eventEmitter: EventEmitter;
5
+
6
+ constructor() {
7
+ this.#eventEmitter = new EventEmitter();
8
+ }
9
+
10
+ on<K extends keyof EventTypes>(eventName: K, listener: (...args: EventTypes[K]) => void) {
11
+ this.#eventEmitter.on(eventName as string, listener);
12
+ }
13
+
14
+ async emit<K extends keyof EventTypes>(eventName: K, ...args: EventTypes[K]) {
15
+ const listeners = this.#eventEmitter.listeners(eventName as string);
16
+ for (const listener of listeners) {
17
+ await listener(...args);
18
+ }
19
+ }
20
+ }
@@ -1,7 +1,7 @@
1
- import { IRpdServer } from "./server";
2
-
3
- export interface FacilityFactory {
4
- name: string;
5
-
6
- createFacility: (server: IRpdServer, options?: any) => Promise<any>;
7
- }
1
+ import { IRpdServer } from "./server";
2
+
3
+ export interface FacilityFactory {
4
+ name: string;
5
+
6
+ createFacility: (server: IRpdServer, options?: any) => Promise<any>;
7
+ }
@@ -1,89 +1,89 @@
1
- import type { RapidRequest } from "../request";
2
-
3
- export type BodyData = Record<string, string | File | (string | File)[]>;
4
- export type ParseBodyOptions = {
5
- /**
6
- * Parse all fields with multiple values should be parsed as an array.
7
- * @default false
8
- * @example
9
- * ```ts
10
- * const data = new FormData()
11
- * data.append('file', 'aaa')
12
- * data.append('file', 'bbb')
13
- * data.append('message', 'hello')
14
- * ```
15
- *
16
- * If `all` is `false`:
17
- * parseBody should return `{ file: 'bbb', message: 'hello' }`
18
- *
19
- * If `all` is `true`:
20
- * parseBody should return `{ file: ['aaa', 'bbb'], message: 'hello' }`
21
- */
22
- all?: boolean;
23
- };
24
-
25
- export const parseFormDataBody = async <T extends BodyData = BodyData>(request: Request, options: ParseBodyOptions = { all: false }): Promise<T> => {
26
- const contentType = request.headers.get("Content-Type");
27
-
28
- if (isFormDataContent(contentType)) {
29
- return parseFormData<T>(request, options);
30
- }
31
-
32
- return {} as T;
33
- };
34
-
35
- function isFormDataContent(contentType: string | null): boolean {
36
- if (contentType === null) {
37
- return false;
38
- }
39
-
40
- return contentType.startsWith("multipart/form-data") || contentType.startsWith("application/x-www-form-urlencoded");
41
- }
42
-
43
- async function parseFormData<T extends BodyData = BodyData>(request: Request, options: ParseBodyOptions): Promise<T> {
44
- const formData = await (request as Request).formData();
45
-
46
- if (formData) {
47
- return convertFormDataToBodyData<T>(formData, options);
48
- }
49
-
50
- return {} as T;
51
- }
52
-
53
- function convertFormDataToBodyData<T extends BodyData = BodyData>(formData: FormData, options: ParseBodyOptions): T {
54
- const form: BodyData = {};
55
-
56
- formData.forEach((value, key) => {
57
- const shouldParseAllValues = options.all || key.endsWith("[]");
58
-
59
- if (!shouldParseAllValues) {
60
- form[key] = value;
61
- } else {
62
- handleParsingAllValues(form, key, value);
63
- }
64
- });
65
-
66
- return form as T;
67
- }
68
-
69
- const handleParsingAllValues = (form: BodyData, key: string, value: FormDataEntryValue): void => {
70
- if (form[key] && isArrayField(form[key])) {
71
- appendToExistingArray(form[key] as (string | File)[], value);
72
- } else if (form[key]) {
73
- convertToNewArray(form, key, value);
74
- } else {
75
- form[key] = value;
76
- }
77
- };
78
-
79
- function isArrayField(field: unknown): field is (string | File)[] {
80
- return Array.isArray(field);
81
- }
82
-
83
- const appendToExistingArray = (arr: (string | File)[], value: FormDataEntryValue): void => {
84
- arr.push(value);
85
- };
86
-
87
- const convertToNewArray = (form: BodyData, key: string, value: FormDataEntryValue): void => {
88
- form[key] = [form[key] as string | File, value];
89
- };
1
+ import type { RapidRequest } from "../request";
2
+
3
+ export type BodyData = Record<string, string | File | (string | File)[]>;
4
+ export type ParseBodyOptions = {
5
+ /**
6
+ * Parse all fields with multiple values should be parsed as an array.
7
+ * @default false
8
+ * @example
9
+ * ```ts
10
+ * const data = new FormData()
11
+ * data.append('file', 'aaa')
12
+ * data.append('file', 'bbb')
13
+ * data.append('message', 'hello')
14
+ * ```
15
+ *
16
+ * If `all` is `false`:
17
+ * parseBody should return `{ file: 'bbb', message: 'hello' }`
18
+ *
19
+ * If `all` is `true`:
20
+ * parseBody should return `{ file: ['aaa', 'bbb'], message: 'hello' }`
21
+ */
22
+ all?: boolean;
23
+ };
24
+
25
+ export const parseFormDataBody = async <T extends BodyData = BodyData>(request: Request, options: ParseBodyOptions = { all: false }): Promise<T> => {
26
+ const contentType = request.headers.get("Content-Type");
27
+
28
+ if (isFormDataContent(contentType)) {
29
+ return parseFormData<T>(request, options);
30
+ }
31
+
32
+ return {} as T;
33
+ };
34
+
35
+ function isFormDataContent(contentType: string | null): boolean {
36
+ if (contentType === null) {
37
+ return false;
38
+ }
39
+
40
+ return contentType.startsWith("multipart/form-data") || contentType.startsWith("application/x-www-form-urlencoded");
41
+ }
42
+
43
+ async function parseFormData<T extends BodyData = BodyData>(request: Request, options: ParseBodyOptions): Promise<T> {
44
+ const formData = await (request as Request).formData();
45
+
46
+ if (formData) {
47
+ return convertFormDataToBodyData<T>(formData, options);
48
+ }
49
+
50
+ return {} as T;
51
+ }
52
+
53
+ function convertFormDataToBodyData<T extends BodyData = BodyData>(formData: FormData, options: ParseBodyOptions): T {
54
+ const form: BodyData = {};
55
+
56
+ formData.forEach((value, key) => {
57
+ const shouldParseAllValues = options.all || key.endsWith("[]");
58
+
59
+ if (!shouldParseAllValues) {
60
+ form[key] = value;
61
+ } else {
62
+ handleParsingAllValues(form, key, value);
63
+ }
64
+ });
65
+
66
+ return form as T;
67
+ }
68
+
69
+ const handleParsingAllValues = (form: BodyData, key: string, value: FormDataEntryValue): void => {
70
+ if (form[key] && isArrayField(form[key])) {
71
+ appendToExistingArray(form[key] as (string | File)[], value);
72
+ } else if (form[key]) {
73
+ convertToNewArray(form, key, value);
74
+ } else {
75
+ form[key] = value;
76
+ }
77
+ };
78
+
79
+ function isArrayField(field: unknown): field is (string | File)[] {
80
+ return Array.isArray(field);
81
+ }
82
+
83
+ const appendToExistingArray = (arr: (string | File)[], value: FormDataEntryValue): void => {
84
+ arr.push(value);
85
+ };
86
+
87
+ const convertToNewArray = (form: BodyData, key: string, value: FormDataEntryValue): void => {
88
+ form[key] = [form[key] as string | File, value];
89
+ };