@cleverbrush/schema 0.0.6 → 0.0.9

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/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import SchemaValidator from './schemaValidator';
2
- export declare type DefaultSchemaType = 'object' | 'function' | 'number' | 'string' | 'array';
2
+ export declare type DefaultSchemaType = 'object' | 'function' | 'number' | 'string' | 'array' | 'alias';
3
3
  export declare type PropType<TObj, TProp extends keyof TObj> = TObj[TProp];
4
4
  export declare type DefaultPropertyDefinition = {
5
5
  isRequired: boolean;
@@ -24,6 +24,10 @@ export declare type ObjectSchemaDefinition<T> = Omit<SchemaDefintion<T>, 'type'>
24
24
  [S in keyof T]: Schema<PropType<T, S>>;
25
25
  }>;
26
26
  };
27
+ export declare type AliasSchemaDefinition<T> = Omit<SchemaDefintion<T>, 'type'> & {
28
+ type: 'alias';
29
+ schemaName: string;
30
+ };
27
31
  export declare type ObjectSchemaDefinitionParam<T> = Omit<ObjectSchemaDefinition<T>, 'type'>;
28
32
  export declare type ParamsValidators<TFunc extends (...args: any) => any> = {
29
33
  [S in keyof Parameters<TFunc>]: SingleSchema<Parameters<TFunc>[S]>;
@@ -53,7 +57,7 @@ export declare type ArraySchemaDefinition<TObj> = Omit<SchemaDefintion<TObj>, 't
53
57
  minLength?: number;
54
58
  maxLength?: number;
55
59
  };
56
- export declare type CompositeSchema<TObj> = TObj extends (...args: any) => any ? FunctionSchemaDefinition<TObj> : TObj extends number ? NumberSchemaDefinition<TObj> : TObj extends string ? StringSchemaDefinition<TObj> | 'string' : NumberSchemaDefinition<TObj> | StringSchemaDefinition<TObj> | ArraySchemaDefinition<TObj> | ObjectSchemaDefinition<TObj>;
60
+ export declare type CompositeSchema<TObj> = TObj extends (...args: any) => any ? FunctionSchemaDefinition<TObj> : TObj extends number ? NumberSchemaDefinition<TObj> : TObj extends string ? StringSchemaDefinition<TObj> | 'string' : NumberSchemaDefinition<TObj> | StringSchemaDefinition<TObj> | ArraySchemaDefinition<TObj> | ObjectSchemaDefinition<TObj> | AliasSchemaDefinition<TObj>;
57
61
  export declare type SingleSchema<TObj = Record<string, never>> = number | string | DefaultSchemaType | CompositeSchema<TObj>;
58
62
  export declare type Schema<TObj> = SingleSchema<TObj> | Array<SingleSchema<TObj>>;
59
63
  export interface ISchemaActions<K, T extends keyof K> {
@@ -95,6 +95,11 @@ class SchemaValidator {
95
95
  throw new Error('not implemented');
96
96
  }
97
97
  async checkValidators(schema, value) {
98
+ if (!schema.isRequired && typeof value === 'undefined') {
99
+ return {
100
+ valid: true
101
+ };
102
+ }
98
103
  if (Array.isArray(schema.validators)) {
99
104
  const validatorsResults = await Promise.allSettled(schema.validators.map((v) => Promise.resolve(v(value))));
100
105
  const rejections = validatorsResults
@@ -128,6 +133,10 @@ class SchemaValidator {
128
133
  return await this.validateDefaultType(schema, obj);
129
134
  }
130
135
  else if (typeof this.schemas[schema] !== 'undefined') {
136
+ if (Array.isArray(this.schemas[schema].schema)) {
137
+ return this.validate(this.schemas[schema]
138
+ .schema, obj);
139
+ }
131
140
  const objSchema = (0, deep_1.deepExtend)(defaultSchemas.object, this.schemas[schema].schema);
132
141
  const res = await (0, validateObject_1.validateObject)(obj, objSchema, this);
133
142
  if (!res.valid)
@@ -162,6 +171,26 @@ class SchemaValidator {
162
171
  if (isDefaultType(schema.type)) {
163
172
  return await this.validateDefaultType(schema.type, obj, schema);
164
173
  }
174
+ else if (schema.type === 'alias') {
175
+ const alias = this.schemas[schema.schemaName]
176
+ .schema;
177
+ if (Array.isArray(alias)) {
178
+ if ((!schema.isRequired && typeof obj === 'undefined') ||
179
+ (schema.isNullable && obj === null)) {
180
+ return {
181
+ valid: true
182
+ };
183
+ }
184
+ return await this.validate(alias, obj);
185
+ }
186
+ if (typeof alias !== 'object')
187
+ throw new Error('it is only possible to use a full schema schema definition as alias');
188
+ const schemaToMerge = { ...schema };
189
+ delete schemaToMerge.type;
190
+ delete schemaToMerge.schemaName;
191
+ const finalSchema = (0, deep_1.deepExtend)(alias, schemaToMerge);
192
+ return await this.validate(finalSchema, obj);
193
+ }
165
194
  else if (schema.type === 'object') {
166
195
  const preliminaryResult = await (0, validateObject_1.validateObject)(obj, schema, this);
167
196
  if (!preliminaryResult.valid)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cleverbrush/schema",
3
- "version": "0.0.6",
3
+ "version": "0.0.9",
4
4
  "keywords": [
5
5
  "object schema validator",
6
6
  "schema",
@@ -19,7 +19,7 @@
19
19
  "build": "tsc"
20
20
  },
21
21
  "dependencies": {
22
- "@cleverbrush/deep": "0.0.6"
22
+ "@cleverbrush/deep": "0.0.9"
23
23
  },
24
24
  "types": "./dist/index.d.ts"
25
25
  }
package/src/index.ts CHANGED
@@ -5,7 +5,8 @@ export type DefaultSchemaType =
5
5
  | 'function'
6
6
  | 'number'
7
7
  | 'string'
8
- | 'array';
8
+ | 'array'
9
+ | 'alias';
9
10
 
10
11
  export type PropType<TObj, TProp extends keyof TObj> = TObj[TProp];
11
12
 
@@ -40,6 +41,11 @@ export type ObjectSchemaDefinition<T> = Omit<SchemaDefintion<T>, 'type'> & {
40
41
  }>;
41
42
  };
42
43
 
44
+ export type AliasSchemaDefinition<T> = Omit<SchemaDefintion<T>, 'type'> & {
45
+ type: 'alias';
46
+ schemaName: string;
47
+ };
48
+
43
49
  export type ObjectSchemaDefinitionParam<T> = Omit<
44
50
  ObjectSchemaDefinition<T>,
45
51
  'type'
@@ -100,7 +106,8 @@ export type CompositeSchema<TObj> = TObj extends (...args: any) => any
100
106
  | NumberSchemaDefinition<TObj>
101
107
  | StringSchemaDefinition<TObj>
102
108
  | ArraySchemaDefinition<TObj>
103
- | ObjectSchemaDefinition<TObj>;
109
+ | ObjectSchemaDefinition<TObj>
110
+ | AliasSchemaDefinition<TObj>;
104
111
 
105
112
  export type SingleSchema<TObj = Record<string, never>> =
106
113
  | number
@@ -912,3 +912,293 @@ test('Validate - one of - 1', async () => {
912
912
  );
913
913
  expect(result).toHaveProperty('valid', true);
914
914
  });
915
+
916
+ test('Validate schema - 1', async () => {
917
+ const authorsReportSpecificationSchema = {
918
+ properties: {
919
+ type: {
920
+ type: 'string',
921
+ equals: 'author'
922
+ },
923
+ start: 'Date',
924
+ end: 'Date',
925
+ selectionStart: 'Date',
926
+ selectionEnd: 'Date',
927
+ granularity: ['day', 'week', 'month'],
928
+ metrics: {
929
+ type: 'array',
930
+ ofType: [
931
+ 'articlesPublished',
932
+ 'searchReferrers',
933
+ 'socialReferrers',
934
+ 'views',
935
+ 'visitors',
936
+ 'newVisitors'
937
+ ]
938
+ },
939
+ filters: {
940
+ type: 'object',
941
+ properties: {
942
+ author: 'AuthorFilter'
943
+ // publication: 'PublicationFilter',
944
+ // article: 'ArticleFilter'
945
+ }
946
+ }
947
+ },
948
+ validators: [
949
+ (value) =>
950
+ value.start <= value.end &&
951
+ value.selectionStart <= value.selectionEnd &&
952
+ value.selectionStart >= value.start &&
953
+ value.selectionStart <= value.end &&
954
+ value.selectionEnd >= value.start &&
955
+ value.selectionEnd <= value.end
956
+ ? {
957
+ valid: true
958
+ }
959
+ : {
960
+ valid: false,
961
+ errors: [
962
+ 'selectionStart <=> selectionEnd should be inside the start <=> end interval'
963
+ ]
964
+ }
965
+ ]
966
+ };
967
+
968
+ const validator = new SchemaValidator()
969
+ .addSchemaType('Date', {
970
+ validators: [
971
+ (value) =>
972
+ value instanceof Date && !Number.isNaN(value)
973
+ ? {
974
+ valid: true
975
+ }
976
+ : {
977
+ valid: false,
978
+ errors: ['should be a valid Date object']
979
+ }
980
+ ]
981
+ })
982
+ .addSchemaType('EqualsFilterCondition', {
983
+ properties: {
984
+ operation: {
985
+ type: 'string',
986
+ equals: 'equals'
987
+ },
988
+ value: ['object', 'string', 'number']
989
+ }
990
+ })
991
+ .addSchemaType('GreaterThanFilterCondition', {
992
+ properties: {
993
+ operation: {
994
+ type: 'string',
995
+ equals: 'greater_than'
996
+ },
997
+ value: 'number'
998
+ }
999
+ })
1000
+ .addSchemaType('LessThanFilterCondition', {
1001
+ properties: {
1002
+ operation: {
1003
+ type: 'string',
1004
+ equals: 'less_than'
1005
+ },
1006
+ value: 'number'
1007
+ }
1008
+ })
1009
+ .addSchemaType('ContainsFilterCondition', {
1010
+ properties: {
1011
+ operation: {
1012
+ type: 'string',
1013
+ equals: 'contains'
1014
+ },
1015
+ value: [
1016
+ 'string',
1017
+ 'number',
1018
+ {
1019
+ type: 'array',
1020
+ ofType: ['string', 'number']
1021
+ }
1022
+ ]
1023
+ }
1024
+ })
1025
+ .addSchemaType('LikeFilterCondition', {
1026
+ properties: {
1027
+ operation: 'like',
1028
+ value: 'string'
1029
+ }
1030
+ })
1031
+ .addSchemaType('BetweenFilterCondition', {
1032
+ properties: {
1033
+ operation: 'between',
1034
+ from: 'number',
1035
+ to: 'number'
1036
+ },
1037
+ validators: [
1038
+ (value) =>
1039
+ value.from <= value.to
1040
+ ? { valid: true }
1041
+ : { valid: false, error: ['from must be <= to'] }
1042
+ ]
1043
+ })
1044
+ .addSchemaType('StringFilterCondition', [
1045
+ 'EqualsFilterCondition',
1046
+ 'LikeFilterCondition'
1047
+ ])
1048
+ .addSchemaType('AuthorFilter', {
1049
+ properties: {
1050
+ fullName: 'StringFilterCondition'
1051
+ }
1052
+ })
1053
+ .addSchemaType(
1054
+ 'AuthorsReportSpecification',
1055
+ authorsReportSpecificationSchema as ObjectSchemaDefinitionParam<any>
1056
+ );
1057
+
1058
+ /**
1059
+ * @type {import('smg-iq/editorial-analytics.reports').AuthorsReportSpecification}
1060
+ */
1061
+ let reportSpec = {
1062
+ type: 'author',
1063
+ start: new Date(2021, 0, 1),
1064
+ end: new Date(),
1065
+ selectionStart: new Date(2022, 0, 1),
1066
+ selectionEnd: new Date(2022, 2, 1),
1067
+ granularity: 'day',
1068
+ metrics: [
1069
+ 'articlesPublished',
1070
+ 'searchReferrers',
1071
+ 'socialReferrers',
1072
+ 'views',
1073
+ 'visitors',
1074
+ 'newVisitors'
1075
+ ],
1076
+ filters: {
1077
+ author: {
1078
+ fullName: {
1079
+ operation: 'between',
1080
+ value: 'some name'
1081
+ }
1082
+ }
1083
+ }
1084
+ };
1085
+
1086
+ let result = await validator.schemas.AuthorsReportSpecification.validate(
1087
+ reportSpec
1088
+ );
1089
+
1090
+ expect(result).toHaveProperty('valid', false);
1091
+
1092
+ reportSpec = {
1093
+ type: 'author',
1094
+ start: new Date(2021, 0, 1),
1095
+ end: new Date(),
1096
+ selectionStart: new Date(2022, 0, 1),
1097
+ selectionEnd: new Date(2022, 2, 1),
1098
+ granularity: 'day',
1099
+ metrics: [
1100
+ 'articlesPublished',
1101
+ 'searchReferrers',
1102
+ 'socialReferrers',
1103
+ 'views',
1104
+ 'visitors',
1105
+ 'newVisitors'
1106
+ ],
1107
+ filters: {
1108
+ author: {
1109
+ fullName: {
1110
+ operation: 'equals',
1111
+ value: 'some name'
1112
+ }
1113
+ }
1114
+ }
1115
+ };
1116
+
1117
+ result = await validator.schemas.AuthorsReportSpecification.validate(
1118
+ reportSpec
1119
+ );
1120
+
1121
+ expect(result).toHaveProperty('valid', true);
1122
+ });
1123
+
1124
+ test('Validate schema - 2', async () => {
1125
+ const validator = new SchemaValidator().addSchemaType('Date', {
1126
+ validators: [
1127
+ (value) =>
1128
+ value instanceof Date && !Number.isNaN(value)
1129
+ ? {
1130
+ valid: true
1131
+ }
1132
+ : {
1133
+ valid: false,
1134
+ errors: ['should be a valid Date object']
1135
+ }
1136
+ ]
1137
+ });
1138
+
1139
+ const result = await validator.validate(
1140
+ {
1141
+ type: 'object',
1142
+ properties: {
1143
+ date: {
1144
+ type: 'alias',
1145
+ schemaName: 'Date',
1146
+ isRequired: false
1147
+ }
1148
+ }
1149
+ },
1150
+ {}
1151
+ );
1152
+
1153
+ expect(result).toHaveProperty('valid', true);
1154
+ });
1155
+
1156
+ test('Not required alternative schema alias', async () => {
1157
+ const validator = new SchemaValidator()
1158
+ .addSchemaType('Alternate1', {
1159
+ properties: {
1160
+ a1: 'string'
1161
+ }
1162
+ })
1163
+ .addSchemaType('Alternate2', {
1164
+ properties: {
1165
+ a2: 'string'
1166
+ }
1167
+ })
1168
+ .addSchemaType('Alternate', ['Alternate1', 'Alternate2']);
1169
+
1170
+ let result = await validator.validate(
1171
+ {
1172
+ type: 'alias',
1173
+ isRequired: false,
1174
+ schemaName: 'Alternate'
1175
+ },
1176
+ undefined
1177
+ );
1178
+
1179
+ expect(result).toHaveProperty('valid', true);
1180
+
1181
+ result = await validator.validate(
1182
+ {
1183
+ type: 'alias',
1184
+ isRequired: false,
1185
+ schemaName: 'Alternate'
1186
+ },
1187
+ {
1188
+ a2: 'something'
1189
+ }
1190
+ );
1191
+
1192
+ expect(result).toHaveProperty('valid', true);
1193
+
1194
+ result = await validator.validate(
1195
+ {
1196
+ type: 'alias',
1197
+ isRequired: false,
1198
+ schemaName: 'Alternate'
1199
+ },
1200
+ 'invalid'
1201
+ );
1202
+
1203
+ expect(result).toHaveProperty('valid', false);
1204
+ });
@@ -156,6 +156,11 @@ export default class SchemaValidator<T = Record<string, never>>
156
156
  schema: CompositeSchema<Record<string, never>>,
157
157
  value: any
158
158
  ): Promise<ValidationResult> {
159
+ if (!schema.isRequired && typeof value === 'undefined') {
160
+ return {
161
+ valid: true
162
+ };
163
+ }
159
164
  if (Array.isArray(schema.validators)) {
160
165
  const validatorsResults = await Promise.allSettled(
161
166
  schema.validators.map((v) => Promise.resolve(v(value)))
@@ -202,6 +207,13 @@ export default class SchemaValidator<T = Record<string, never>>
202
207
  obj
203
208
  );
204
209
  } else if (typeof this.schemas[schema as keyof T] !== 'undefined') {
210
+ if (Array.isArray(this.schemas[schema as keyof T].schema)) {
211
+ return this.validate(
212
+ this.schemas[schema as keyof T]
213
+ .schema as any as Schema<any>,
214
+ obj
215
+ );
216
+ }
205
217
  const objSchema = deepExtend(
206
218
  defaultSchemas.object,
207
219
  this.schemas[schema as keyof T].schema
@@ -244,6 +256,33 @@ export default class SchemaValidator<T = Record<string, never>>
244
256
  obj,
245
257
  schema
246
258
  );
259
+ } else if (schema.type === 'alias') {
260
+ const alias = this.schemas[schema.schemaName]
261
+ .schema as Schema<any>;
262
+ if (Array.isArray(alias)) {
263
+ if (
264
+ (!schema.isRequired && typeof obj === 'undefined') ||
265
+ (schema.isNullable && obj === null)
266
+ ) {
267
+ return {
268
+ valid: true
269
+ };
270
+ }
271
+ return await this.validate(alias, obj);
272
+ }
273
+ if (typeof alias !== 'object')
274
+ throw new Error(
275
+ 'it is only possible to use a full schema schema definition as alias'
276
+ );
277
+
278
+ const schemaToMerge = { ...schema };
279
+ delete schemaToMerge.type;
280
+ delete schemaToMerge.schemaName;
281
+ const finalSchema: Schema<any> = deepExtend(
282
+ alias,
283
+ schemaToMerge
284
+ );
285
+ return await this.validate(finalSchema, obj);
247
286
  } else if (schema.type === 'object') {
248
287
  const preliminaryResult = await validateObject(
249
288
  obj,