@lobb-js/core 0.14.0 → 0.15.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobb-js/core",
3
- "version": "0.14.0",
3
+ "version": "0.15.1",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -77,7 +77,15 @@ export class WebServer {
77
77
  this.server = Bun.serve({
78
78
  port: this.webConfig.port ?? 3000,
79
79
  hostname: this.webConfig.host ?? "0.0.0.0",
80
- fetch: this.app.fetch,
80
+ fetch: (req, server) => {
81
+ // Bun closes idle connections after 10s by default.
82
+ // SSE connections (/api/events) are long-lived and may have quiet periods,
83
+ // so we disable the timeout per-request only for that route.
84
+ if (new URL(req.url).pathname.startsWith("/api/events")) {
85
+ server.timeout(req, 0);
86
+ }
87
+ return this.app.fetch(req);
88
+ },
81
89
  });
82
90
  this.port = this.server.port!;
83
91
  }
@@ -15,8 +15,28 @@ const UiSchema = z.object({
15
15
  input: UiInputSchema.optional(),
16
16
  });
17
17
 
18
+ export type FieldHookFn = (ctx: {
19
+ data: Record<string, any>;
20
+ context?: any;
21
+ }) => any;
22
+
23
+ export type FieldValidatorFn = (ctx: {
24
+ value: any;
25
+ data: Record<string, any>;
26
+ context?: any;
27
+ }) => string | undefined | Promise<string | undefined>;
28
+
29
+ const FieldHooksSchema = z.object({
30
+ beforeCreate: z.custom<FieldHookFn>((val) => typeof val === "function").optional(),
31
+ beforeUpdate: z.custom<FieldHookFn>((val) => typeof val === "function").optional(),
32
+ afterCreate: z.custom<FieldHookFn>((val) => typeof val === "function").optional(),
33
+ afterUpdate: z.custom<FieldHookFn>((val) => typeof val === "function").optional(),
34
+ }).optional();
35
+
18
36
  // Base field schema
19
37
  export const CollectionFieldBaseSchema = z.object({
38
+ hooks: FieldHooksSchema,
39
+ validator: z.custom<FieldValidatorFn>((val) => typeof val === "function").optional(),
20
40
  pre_processors: z.record(z.any()).optional(),
21
41
  post_processors: z.record(z.any()).optional(),
22
42
  validators: z.record(z.any()).optional(),
@@ -4,6 +4,18 @@ import {
4
4
  CollectionIntegerFieldSchema,
5
5
  } from "./collectionFields.ts";
6
6
 
7
+ export type CollectionHookFn = (ctx: {
8
+ data: Record<string, any>;
9
+ context?: any;
10
+ }) => void | Promise<void>;
11
+
12
+ const CollectionHooksSchema = z.object({
13
+ beforeCreate: z.custom<CollectionHookFn>((val) => typeof val === "function").optional(),
14
+ afterCreate: z.custom<CollectionHookFn>((val) => typeof val === "function").optional(),
15
+ beforeUpdate: z.custom<CollectionHookFn>((val) => typeof val === "function").optional(),
16
+ afterUpdate: z.custom<CollectionHookFn>((val) => typeof val === "function").optional(),
17
+ }).optional();
18
+
7
19
  // CollectionIndexField
8
20
  export const CollectionIndexFieldSchema = z.object({
9
21
  order: z.enum(["asc", "desc"]),
@@ -33,6 +45,7 @@ export const CollectionFieldsSchema = z.intersection(
33
45
  export const CollectionConfigSchema = z.object({
34
46
  category: z.string().optional(),
35
47
  singleton: z.boolean().optional(),
48
+ hooks: CollectionHooksSchema,
36
49
  indexes: CollectionIndexesSchema,
37
50
  fields: CollectionFieldsSchema,
38
51
  });
@@ -3,13 +3,18 @@ import { getCollectionsTableWorkflows } from "./collectionsTable/index.ts";
3
3
  import { getUtilsCoreWorkflows } from "./utilsCoreWorkflows.ts";
4
4
  import { postOperationsWorkflows } from "./processors/postOperationsWorkflows.ts";
5
5
  import { preOperationsWorkflows } from "./processors/preOperationsWorkflows.ts";
6
+ import { hooksWorkflows } from "./processors/hooksWorkflows.ts";
7
+ import { validatorWorkflows } from "./processors/validatorWorkflows.ts";
6
8
  import { coreWorkflowsCollectionWorkflows } from "./workflowsCollection/workflowsCollectionWorkflows.ts";
7
9
  import { getQueryCoreWorkflows } from "./queryCoreWorkflows.ts";
8
10
 
9
11
  export function getCoreWorkflows(): Array<Workflow> {
10
12
  return [
13
+ ...hooksWorkflows.filter((w) => w.name.startsWith("core_hooksBefore")),
14
+ ...validatorWorkflows,
11
15
  ...preOperationsWorkflows,
12
16
  ...postOperationsWorkflows,
17
+ ...hooksWorkflows.filter((w) => w.name.startsWith("core_hooksAfter")),
13
18
  ...coreWorkflowsCollectionWorkflows,
14
19
  ...getCollectionsTableWorkflows(),
15
20
  ...getQueryCoreWorkflows(),
@@ -0,0 +1,60 @@
1
+ import type { Workflow } from "../../WorkflowSystem.ts";
2
+ import { Lobb } from "../../../Lobb.ts";
3
+
4
+ async function runFieldHooks(
5
+ hookName: "beforeCreate" | "beforeUpdate" | "afterCreate" | "afterUpdate",
6
+ input: any,
7
+ ) {
8
+ const fields = Lobb.instance.configManager.getCollection(input.collectionName).fields;
9
+ for (const [fieldName, fieldConfig] of Object.entries(fields)) {
10
+ const hook = fieldConfig.hooks?.[hookName];
11
+ if (!hook) continue;
12
+ const result = await hook({ data: input.data, context: input.context });
13
+ if (result !== undefined) {
14
+ input.data[fieldName] = result;
15
+ }
16
+ }
17
+ }
18
+
19
+ export const hooksWorkflows: Workflow[] = [
20
+ {
21
+ name: "core_hooksBeforeCreate",
22
+ eventName: "core.store.preCreateOne",
23
+ handler: async (input) => {
24
+ const hooks = Lobb.instance.configManager.getCollection(input.collectionName).hooks;
25
+ await hooks?.beforeCreate?.({ data: input.data, context: input.context });
26
+ await runFieldHooks("beforeCreate", input);
27
+ return input;
28
+ },
29
+ },
30
+ {
31
+ name: "core_hooksBeforeUpdate",
32
+ eventName: "core.store.preUpdateOne",
33
+ handler: async (input) => {
34
+ const hooks = Lobb.instance.configManager.getCollection(input.collectionName).hooks;
35
+ await hooks?.beforeUpdate?.({ data: input.data, context: input.context });
36
+ await runFieldHooks("beforeUpdate", input);
37
+ return input;
38
+ },
39
+ },
40
+ {
41
+ name: "core_hooksAfterCreate",
42
+ eventName: "core.store.createOne",
43
+ handler: async (input) => {
44
+ const hooks = Lobb.instance.configManager.getCollection(input.collectionName).hooks;
45
+ await hooks?.afterCreate?.({ data: input.data, context: input.context });
46
+ await runFieldHooks("afterCreate", input);
47
+ return input;
48
+ },
49
+ },
50
+ {
51
+ name: "core_hooksAfterUpdate",
52
+ eventName: "core.store.updateOne",
53
+ handler: async (input) => {
54
+ const hooks = Lobb.instance.configManager.getCollection(input.collectionName).hooks;
55
+ await hooks?.afterUpdate?.({ data: input.data, context: input.context });
56
+ await runFieldHooks("afterUpdate", input);
57
+ return input;
58
+ },
59
+ },
60
+ ];
@@ -87,6 +87,7 @@ export async function validatePayloadWithSchema(
87
87
  }
88
88
  }
89
89
  }
90
+
90
91
  }
91
92
  }
92
93
 
@@ -0,0 +1,54 @@
1
+ import type { Workflow } from "../../WorkflowSystem.ts";
2
+ import { Lobb } from "../../../Lobb.ts";
3
+ import { LobbError } from "../../../LobbError.ts";
4
+
5
+ async function runFieldValidators(input: any, processAllFields: boolean) {
6
+ const fields = Lobb.instance.configManager.getCollection(input.collectionName).fields;
7
+ const data = input.data;
8
+ const errors: Record<string, string[]> = {};
9
+
10
+ const fieldNames = processAllFields ? Object.keys(fields) : Object.keys(data);
11
+
12
+ for (const fieldName of fieldNames) {
13
+ const fieldConfig = fields[fieldName];
14
+ if (!fieldConfig?.validator) continue;
15
+
16
+ const error = await fieldConfig.validator({
17
+ value: data[fieldName],
18
+ data,
19
+ context: input.context,
20
+ });
21
+
22
+ if (error) {
23
+ if (!errors[fieldName]) errors[fieldName] = [];
24
+ errors[fieldName].push(error);
25
+ }
26
+ }
27
+
28
+ if (Object.keys(errors).length) {
29
+ throw new LobbError({
30
+ code: "BAD_REQUEST",
31
+ message: "Validation failed. Please check your inputs and try again.",
32
+ details: errors,
33
+ });
34
+ }
35
+
36
+ return input;
37
+ }
38
+
39
+ export const validatorWorkflows: Workflow[] = [
40
+ {
41
+ name: "core_validatorOnCreate",
42
+ eventName: "core.store.preCreateOne",
43
+ handler: async (input) => {
44
+ return await runFieldValidators(input, true);
45
+ },
46
+ },
47
+ {
48
+ name: "core_validatorOnUpdate",
49
+ eventName: "core.store.preUpdateOne",
50
+ handler: async (input) => {
51
+ return await runFieldValidators(input, false);
52
+ },
53
+ },
54
+ ];