@lobb-js/core 0.22.1 → 0.23.0

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,7 +1,7 @@
1
1
  {
2
2
  "name": "@lobb-js/core",
3
3
  "license": "UNLICENSED",
4
- "version": "0.22.1",
4
+ "version": "0.23.0",
5
5
  "type": "module",
6
6
  "publishConfig": {
7
7
  "access": "public"
package/src/Lobb.ts CHANGED
@@ -144,6 +144,7 @@ export class Lobb {
144
144
  process.chdir(config.project.context ?? ".");
145
145
 
146
146
  await Lobb.instance.extensionSystem.loadExtensionsCollections();
147
+ this.configManager.addUniqueIndexesFromFields();
147
148
  this.configManager.addRelationsFromIntegerRefrences();
148
149
  }
149
150
  }
@@ -42,6 +42,31 @@ export class ConfigManager {
42
42
  };
43
43
  }
44
44
 
45
+ public addUniqueIndexesFromFields() {
46
+ for (
47
+ const [collectionName, collectionValue] of Object.entries(
48
+ this.config.collections,
49
+ )
50
+ ) {
51
+ if (collectionValue.virtual) continue;
52
+ for (
53
+ const [fieldName, fieldValue] of Object.entries(
54
+ (collectionValue as any).fields,
55
+ )
56
+ ) {
57
+ if (fieldValue.unique) {
58
+ const indexName = `${collectionName}_${fieldName}_unique_index`;
59
+ (collectionValue as any).indexes[indexName] = {
60
+ unique: true,
61
+ fields: {
62
+ [fieldName]: { order: "asc" },
63
+ },
64
+ };
65
+ }
66
+ }
67
+ }
68
+ }
69
+
45
70
  public addRelationsFromIntegerRefrences() {
46
71
  for (
47
72
  const [collectionName, collectionValue] of Object.entries(
@@ -54,6 +54,7 @@ const FieldHooksSchema = z.object({
54
54
  export const CollectionFieldBaseSchema = z.object({
55
55
  virtual: z.never().optional(),
56
56
  required: z.boolean().optional(),
57
+ unique: z.boolean().optional(),
57
58
  hooks: FieldHooksSchema,
58
59
  validator: z.custom<FieldValidatorFn>((val) => typeof val === "function").optional(),
59
60
  ui: UiSchema.optional(),
@@ -6,11 +6,13 @@ import { validatorWorkflows } from "./processors/validatorWorkflows.ts";
6
6
  import { requiredWorkflows } from "./processors/requiredWorkflows.ts";
7
7
  import { defaultWorkflows } from "./processors/defaultWorkflows.ts";
8
8
  import { enumWorkflows } from "./processors/enumWorkflows.ts";
9
+ import { coerceAndValidateWorkflows } from "./processors/coerceAndValidateWorkflows.ts";
9
10
  import { coreWorkflowsCollectionWorkflows } from "./workflowsCollection/workflowsCollectionWorkflows.ts";
10
11
  import { getQueryCoreWorkflows } from "./queryCoreWorkflows.ts";
11
12
 
12
13
  export function getCoreWorkflows(): Array<Workflow> {
13
14
  return [
15
+ ...coerceAndValidateWorkflows,
14
16
  ...hooksWorkflows.filter((w) => w.name.startsWith("core_hooksBefore")),
15
17
  ...defaultWorkflows,
16
18
  ...requiredWorkflows,
@@ -0,0 +1,108 @@
1
+ import type { Workflow } from "../../WorkflowSystem.ts";
2
+ import { Lobb } from "../../../Lobb.ts";
3
+ import { LobbError } from "../../../LobbError.ts";
4
+
5
+ async function coerceAndValidate(input: any) {
6
+ if (Lobb.instance.configManager.isCollectionVirtual(input.collectionName)) return input;
7
+ const fields = Lobb.instance.configManager.getNormalCollection(input.collectionName).fields;
8
+ const data = input.data;
9
+ const errors: Record<string, string[]> = {};
10
+
11
+ for (const [fieldName, fieldConfig] of Object.entries(fields) as [string, any][]) {
12
+ if (!(fieldName in data)) continue;
13
+ const value = data[fieldName];
14
+
15
+ try {
16
+ if (value === null || value === undefined) continue;
17
+
18
+ if (fieldConfig.type === "string") {
19
+ if (typeof value !== "string") {
20
+ data[fieldName] = String(value);
21
+ }
22
+ } else if (fieldConfig.type === "text") {
23
+ if (typeof value !== "string") {
24
+ data[fieldName] = String(value);
25
+ }
26
+ } else if (fieldConfig.type === "integer") {
27
+ if (value === "") {
28
+ data[fieldName] = null;
29
+ } else {
30
+ const int = Number(value);
31
+ if (!Number.isFinite(int)) throw new Error(`${fieldName} must be a valid integer`);
32
+ data[fieldName] = Math.trunc(int);
33
+ }
34
+ } else if (fieldConfig.type === "long") {
35
+ if (value === "") {
36
+ data[fieldName] = null;
37
+ } else if (typeof value === "string" && !/^-?\d+$/.test(value.trim())) {
38
+ throw new Error(`${fieldName} must be a valid integer`);
39
+ }
40
+ } else if (fieldConfig.type === "float") {
41
+ if (value === "") {
42
+ data[fieldName] = null;
43
+ } else {
44
+ const float = Number(value);
45
+ if (!Number.isFinite(float)) throw new Error(`${fieldName} must be a valid number`);
46
+ data[fieldName] = float;
47
+ }
48
+ } else if (fieldConfig.type === "decimal") {
49
+ if (value === "") {
50
+ data[fieldName] = null;
51
+ } else if (typeof value === "string" && isNaN(Number(value))) {
52
+ throw new Error(`${fieldName} must be a valid decimal number`);
53
+ }
54
+ } else if (fieldConfig.type === "bool") {
55
+ if (value === "") {
56
+ data[fieldName] = null;
57
+ } else if (typeof value === "boolean") {
58
+ // already correct, nothing to do
59
+ } else if (typeof value === "string" && value.toLowerCase() === "true" || value === "1" || value === 1) {
60
+ data[fieldName] = true;
61
+ } else if (typeof value === "string" && value.toLowerCase() === "false" || value === "0" || value === 0) {
62
+ data[fieldName] = false;
63
+ } else {
64
+ throw new Error(`${fieldName} must be a valid boolean`);
65
+ }
66
+ } else if (fieldConfig.type === "date") {
67
+ if (value === "") {
68
+ data[fieldName] = null;
69
+ }
70
+ } else if (fieldConfig.type === "datetime") {
71
+ if (value === "") {
72
+ data[fieldName] = null;
73
+ }
74
+ } else if (fieldConfig.type === "time") {
75
+ if (value === "") {
76
+ data[fieldName] = null;
77
+ }
78
+ } else {
79
+ throw new Error(`coerceAndValidate: type "${fieldConfig.type}" is not implemented`);
80
+ }
81
+ } catch (e: any) {
82
+ errors[fieldName] = [e.message];
83
+ }
84
+ }
85
+
86
+ if (Object.keys(errors).length) {
87
+ throw new LobbError({
88
+ code: "BAD_REQUEST",
89
+ message: "Validation failed. Please check your inputs and try again.",
90
+ details: errors,
91
+ });
92
+ }
93
+
94
+ return input;
95
+ }
96
+
97
+ export const coerceAndValidateWorkflows: Workflow[] = [
98
+ {
99
+ name: "core_coerceAndValidateOnCreate",
100
+ eventName: "core.store.preCreateOne",
101
+ handler: async (input) => coerceAndValidate(input),
102
+ },
103
+ {
104
+ name: "core_coerceAndValidateOnUpdate",
105
+ eventName: "core.store.preUpdateOne",
106
+ handler: async (input) => coerceAndValidate(input),
107
+ },
108
+ ];