@tellescope/schema 0.0.7 → 0.0.8

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/src/schema.ts CHANGED
@@ -1,7 +1,29 @@
1
- import "@tellescope/types"
2
- // import "@tellescope/types-server"
1
+ import {
2
+ ServerModelForName,
3
+ DatabaseModel,
4
+ DatabaseRecord,
5
+ Enduser,
6
+ ObjectId,
7
+ ModelName,
8
+ } from "@tellescope/types-server"
9
+ import {
10
+ ErrorInfo,
11
+ Indexable,
12
+ Operation,
13
+ JSONType,
14
+ CRUD,
15
+ HTTPMethod,
16
+ } from "@tellescope/types-utilities"
17
+ import {
18
+ EnduserSession,
19
+ ConfiguredSession,
20
+ JourneyState,
21
+ UserSession,
22
+ } from "@tellescope/types-models"
3
23
 
4
24
  import {
25
+ EscapeBuilder,
26
+
5
27
  booleanValidator,
6
28
  dateValidator,
7
29
  emailValidator,
@@ -33,6 +55,8 @@ import {
33
55
  stringValidator250,
34
56
  stringValidator5000,
35
57
  listOfDisplayNameInfo,
58
+ fileTypeValidator,
59
+ fileSizeValidator,
36
60
  } from "@tellescope/validation"
37
61
 
38
62
  import {
@@ -40,123 +64,126 @@ import {
40
64
  DEFAULT_OPERATIONS,
41
65
  PLACEHOLDER_ID,
42
66
  } from "@tellescope/constants"
67
+ export type RelationshipConstraint<T> = {
68
+ explanation: string; // human readable, for documentation purposes
69
+ evaluate: (v: T, dependencies: Indexable<Partial<DatabaseModel>>) => string | void;
70
+ }
71
+
72
+ export type DependencyAccessConstraint <T> = { type: 'dependency', foreignModel: ModelName, foreignField: string, accessField: keyof T }
73
+
74
+ export type AccessConstraint <T> = { type: 'creatorOnly' }
75
+ | { type: 'filter', field: string }
76
+ | DependencyAccessConstraint<T>
77
+
78
+ export type UniqueArrayConstraint <T> = { array: keyof T, itemKey?: string }
79
+
80
+ export type Constraint <T> = {
81
+ unique: (keyof T & string | UniqueArrayConstraint<T>)[];
82
+ globalUnique?: (keyof T)[];
83
+ relationship: RelationshipConstraint<Partial<T>>[];
84
+ access?: AccessConstraint<T>[];
85
+ }
86
+
87
+ export type Initializer <T, R> = (a: T, s: ConfiguredSession | EnduserSession) => R
88
+
89
+ export type EndpointOptions = {
90
+ // parameters used for endpoint that aren't stored in the model
91
+ parameters?: { [index: string]: EscapeBuilder<any> },
92
+ }
93
+
94
+ export type DependencyDeletionAction = 'delete' | 'unset' | 'setNull' | 'nop'
95
+ export type DependecyRelationship = 'foreignKey' | 'value'
96
+
97
+ export type Dependency <T=DatabaseRecord> = {
98
+ dependsOn: ModelName[], // list of => OR, multiple dependency records => AND
99
+ dependencyField: string,
100
+ relationship: DependecyRelationship,
101
+ onDependencyDelete: DependencyDeletionAction,
102
+ getDependentValues?: (t: T) => JSONType[], // for accessing the values of a Dependency
103
+ filterByDependency?: (foreignValue: JSONType, foreignModel?: DatabaseModel) => { // for filtering against a Dependency
104
+ field: string,
105
+ value: JSONType | 'any',
106
+ },
107
+ }
108
+
109
+ export type ModelFieldInfo <T, R> = {
110
+ validator: EscapeBuilder<R>,
111
+ readonly?: boolean,
112
+ required?: boolean,
113
+ updatesDisabled?: boolean,
114
+ examples?: JSONType[],
115
+ initializer?: Initializer<Partial<T>, R>, // should include the required fields of T, not just partial
116
+ dependencies?: Dependency<Partial<T>>[],
117
+ }
118
+
119
+ export type ModelFields<T> = {
120
+ [K in keyof T]: ModelFieldInfo<T, T[K]>
121
+ }
122
+ export type extractFields<Type> = Type extends ModelFields<infer X> ? X : never
123
+
124
+ type ArgumentInfo = {
125
+ description?: string;
126
+ }
127
+
128
+ type ActionInfo = {
129
+ name?: string,
130
+ description?: string,
131
+ notes?: string[],
132
+ warnings?: string[],
133
+ }
134
+
135
+ type CustomAction <P=any, R=any> = {
136
+ op: Operation | 'custom',
137
+ access: CRUD,
138
+ // parameters: InputValidation<P>,
139
+ parameters: ModelFields<P>,
140
+ returns: R extends Array<any> ? ModelFieldInfo<any, R> : ModelFields<R>,
141
+ path?: string,
142
+ method?: HTTPMethod,
143
+ enduserOnly?: boolean,
144
+ } & ActionInfo
145
+
146
+ export type EnduserAction = {
147
+ field?: string,
148
+ } & ActionInfo
149
+
150
+ type CustomActionsForModel = {
151
+ [K in ModelName]: { [index: string]: CustomAction }
152
+ }
153
+
154
+ type ReadFilter <T> = { [K in keyof T]?: { required: boolean } }
43
155
 
44
- // type RelationshipConstraint<T> = {
45
- // explanation: string; // human readable, for documentation purposes
46
- // evaluate: (v: T, dependencies: Indexable<Partial<DatabaseModel>>) => string | void;
47
- // }
48
-
49
- // type DependencyAccessConstraint <T> = { type: 'dependency', foreignModel: ModelName, foreignField: string, accessField: keyof T }
50
-
51
- // type AccessConstraint <T> = { type: 'creatorOnly' }
52
- // | { type: 'filter', field: string }
53
- // | DependencyAccessConstraint<T>
54
-
55
- // type UniqueArrayConstraint <T> = { array: keyof T, itemKey?: string }
56
-
57
- // type Constraint <T> = {
58
- // unique: (keyof T & string | UniqueArrayConstraint<T>)[];
59
- // globalUnique?: (keyof T)[];
60
- // relationship: RelationshipConstraint<Partial<T>>[];
61
- // access?: AccessConstraint<T>[];
62
- // }
63
-
64
- // type Initializer <T, R> = (a: T, s: ConfiguredSession) => R
65
-
66
- // type EndpointOptions = {
67
- // // parameters used for endpoint that aren't stored in the model
68
- // parameters?: { [index: string]: EscapeBuilder<any> },
69
- // }
70
-
71
- // type DependencyDeletionAction = 'delete' | 'unset' | 'setNull' | 'nop'
72
- // type DependecyRelationship = 'foreignKey' | 'value'
73
-
74
- // type Dependency <T=DatabaseRecord> = {
75
- // dependsOn: ModelName[], // list of => OR, multiple dependency records => AND
76
- // dependencyField: string,
77
- // relationship: DependecyRelationship,
78
- // onDependencyDelete: DependencyDeletionAction,
79
- // getDependentValues?: (t: T) => JSONType[], // for accessing the values of a Dependency
80
- // filterByDependency?: (foreignValue: JSONType, foreignModel?: DatabaseModel) => { // for filtering against a Dependency
81
- // field: string,
82
- // value: JSONType | 'any',
83
- // },
84
- // }
85
-
86
- // type ModelFieldInfo <T, R> = {
87
- // validator: EscapeBuilder<R>,
88
- // readonly?: boolean,
89
- // required?: boolean,
90
- // updatesDisabled?: boolean,
91
- // examples?: JSONType[],
92
- // initializer?: Initializer<Partial<T>, R>, // should include the required fields of T, not just partial
93
- // dependencies?: Dependency<Partial<T>>[],
94
- // }
95
-
96
- // export type ModelFields<T> = {
97
- // [K in keyof T]: ModelFieldInfo<T, T[K]>
98
- // }
99
- // type extractFields<Type> = Type extends ModelFields<infer X> ? X : never
100
-
101
- // type ArgumentInfo = {
102
- // description?: string;
103
- // }
104
-
105
- // type ActionInfo = {
106
- // name?: string,
107
- // description?: string,
108
- // notes?: string[],
109
- // warnings?: string[],
110
- // }
111
-
112
- // type CustomAction <P=any, R=any> = {
113
- // op: Operation | 'custom',
114
- // access: CRUD,
115
- // // parameters: InputValidation<P>,
116
- // parameters: ModelFields<P>,
117
- // returns: ModelFields<R>,
118
- // path?: string,
119
- // method?: HTTPMethod,
120
- // } & ActionInfo
121
-
122
- // type CustomActionsForModel = {
123
- // [K in ModelName]: { [index: string]: CustomAction }
124
- // }
125
-
126
- // type ReadFilter <T> = { [K in keyof T]?: { required: boolean } }
127
-
128
-
129
- // // m is the original model (or undefined, if create)
130
- // // allows for easier event handling based on specific updates (by comparing args to pre-update model)
131
- // type SideEffectHandler <T, O=any> = (args: Partial<T>[], m: (Partial<T> | undefined)[] | undefined, n: (Partial<T> & { _id: ObjectId })[], s: ConfiguredSession, o: O) => Promise<ErrorInfo[]>;
132
-
133
- // type SideEffect = {
134
- // name: string;
135
- // description: string;
136
- // }
137
-
138
- // export type Model<T, N extends ModelName> = {
139
- // info: {
140
- // name?: string,
141
- // description?: string,
142
- // sideEffects?: { [K in Operation]?: SideEffect[] }
143
- // },
144
- // fields: ModelFields<T>,
145
- // constraints: Constraint<T>,
146
- // defaultActions: { [K in Operation]?: ActionInfo },
147
- // customActions: CustomActionsForModel[N],
148
- // readFilter?: ReadFilter<T>,
149
- // options?: {
150
- // create?: EndpointOptions,
151
- // }
152
- // }
153
- // type extractModelType<Type> = Type extends Model<infer T, infer N> ? T : never
154
-
155
- // type DatabaseModelForName = ToServerModels<ModelForName>
156
-
157
- // type Schema = {
158
- // [N in keyof DatabaseModelForName]: Model<DatabaseModelForName[N], ModelName>
159
- // }
156
+
157
+ // m is the original model (or undefined, if create)
158
+ // allows for easier event handling based on specific updates (by comparing args to pre-update model)
159
+ export type SideEffectHandler <T, O=any> = (args: Partial<T>[], m: (Partial<T> | undefined)[] | undefined, n: (Partial<T> & { _id: ObjectId })[], s: ConfiguredSession | EnduserSession, o: O) => Promise<ErrorInfo[]>;
160
+
161
+ type SideEffect = {
162
+ name: string;
163
+ description: string;
164
+ }
165
+
166
+ export type Model<T, N extends ModelName> = {
167
+ info: {
168
+ name?: string,
169
+ description?: string,
170
+ sideEffects?: { [K in Operation]?: SideEffect[] }
171
+ },
172
+ fields: ModelFields<T>,
173
+ constraints: Constraint<T>,
174
+ defaultActions: { [k in Operation]?: ActionInfo },
175
+ enduserActions?: { [index: string]: EnduserAction },
176
+ customActions: CustomActionsForModel[N],
177
+ readFilter?: ReadFilter<T>,
178
+ options?: {
179
+ create?: EndpointOptions,
180
+ }
181
+ }
182
+ export type extractModelType<Type> = Type extends Model<infer T, infer N> ? T : never
183
+
184
+ export type Schema = {
185
+ [N in keyof ServerModelForName]: Model<ServerModelForName[N], ModelName>
186
+ }
160
187
 
161
188
  const sideEffects = {
162
189
  trackJourneyEngagement: {
@@ -203,7 +230,11 @@ export type BuiltInFields_T = typeof BuiltInFields
203
230
  export type CustomActions = {
204
231
  api_keys: {
205
232
  create: CustomAction<{}, { id: string, key: string}>,
206
- }
233
+ },
234
+ files: {
235
+ prepare_file_upload: CustomAction<{ name: string, size: number, type: string }, { presignedUpload: object, file: File }>,
236
+ file_download_URL: CustomAction<{ secureName: string }, { downloadURL: string }>,
237
+ },
207
238
  journeys: {
208
239
  update_state: CustomAction<{ updates: JourneyState, id: string, name: string }, {}>,
209
240
  },
@@ -246,72 +277,26 @@ export const schema: SchemaV1 = {
246
277
  update: [sideEffects.trackJourneyEngagement],
247
278
  }
248
279
  },
249
- customActions: {
250
- set_password: {
251
- op: "custom", access: 'update', method: "post",
252
- name: 'Set enduser password',
253
- path: '/set-enduser-password',
254
- description: "Sets (or resets) an enduser's password. Minimum length 8 characters.",
255
- parameters: {
256
- id: { validator: mongoIdStringValidator, required: true },
257
- password: { validator: stringValidator100, required: true },
258
- },
259
- returns: { } //authToken: { validator: stringValidator5000 } },
260
- },
261
- is_authenticated: {
262
- op: "custom", access: 'read', method: "get",
263
- name: 'Check enduser authentication',
264
- path: '/enduser-is-authenticated',
265
- description: "Checks the validity of an enduser's authToken",
266
- parameters: {
267
- id: { validator: mongoIdStringValidator, required: true },
268
- authToken: { validator: stringValidator5000, required: true },
269
- },
270
- returns: {
271
- isAuthenticated: { validator: booleanValidator, required: true },
272
- enduser: { validator: 'enduser' },
273
- } as any // add enduser eventually, when validator defined
274
- },
275
- refresh_session: {
276
- op: "custom", access: 'update', method: "post",
277
- name: 'Refresh enduser authentication',
278
- path: '/refresh-enduser-session',
279
- description: "When called by an authenticated enduser, generates a new session",
280
- parameters: { },
281
- enduserOnly: true,
282
- returns: {
283
- authToken: { validator: stringValidator, required: true },
284
- enduser: { validator: 'enduser' },
285
- } as any // add enduser eventually, when validator defined
286
- },
287
- logout: {
288
- op: "custom", access: 'update', method: "post",
289
- name: 'Logout enduser',
290
- path: '/logout-enduser',
291
- description: "Logs out an enduser",
292
- parameters: {},
293
- returns: {},
294
- },
280
+ constraints: {
281
+ unique: ['email', 'phone', 'externalId'],
282
+ relationship: [
283
+ {
284
+ explanation: 'One of email or phone is required',
285
+ evaluate: ({ email, phone }) => {
286
+ if (!(email || phone))
287
+ return 'One of email or phone is required'
288
+ }
289
+ }
290
+ ],
295
291
  },
292
+ defaultActions: DEFAULT_OPERATIONS,
296
293
  enduserActions: { logout: {}, refresh_session: {} },
297
- publicActions: {
298
- login: {
299
- op: "custom", access: 'read', method: "post",
300
- name: 'Login enduser',
301
- path: '/login-enduser',
302
- description: "Generates an authentication token for access to enduser-facing endpoints",
303
- enduserOnly: true, // implemented as authenticate in enduser sdk only
304
- parameters: {
305
- id: { validator: mongoIdStringValidator },
306
- password: { validator: stringValidator100, required: true }, // required until optional challenge token available
307
- phone: { validator: phoneValidator },
308
- email: { validator: emailValidator },
309
- },
310
- returns: { authToken: { validator: stringValidator5000 } },
311
- },
312
- },
313
294
  fields: {
314
295
  ...BuiltInFields,
296
+ externalId: {
297
+ validator: stringValidator250,
298
+ examples: ['addfed3e-ddea-415b-b52b-df820c944dbb'],
299
+ },
315
300
  email: {
316
301
  validator: emailValidator,
317
302
  examples: ['test@tellescope.com'],
@@ -374,23 +359,84 @@ export const schema: SchemaV1 = {
374
359
  lastCommunication: {
375
360
  validator: dateValidator,
376
361
  },
362
+ avatar: {
363
+ validator: stringValidator100,
364
+ dependencies: [
365
+ {
366
+ dependsOn: ['files'],
367
+ dependencyField: 'secureName',
368
+ relationship: 'foreignKey',
369
+ onDependencyDelete: 'unset',
370
+ },
371
+ ]
372
+ },
377
373
  // recentMessagePreview: {
378
374
  // validator: stringValidator,
379
375
  // },
380
376
  },
381
- constraints: {
382
- unique: ['email', 'phone'],
383
- relationship: [
384
- {
385
- explanation: 'One of email or phone is required',
386
- evaluate: ({ email, phone }) => {
387
- if (!(email || phone))
388
- return 'One of email or phone is required'
389
- }
390
- }
391
- ],
377
+ customActions: {
378
+ set_password: {
379
+ op: "custom", access: 'update', method: "post",
380
+ name: 'Set enduser password',
381
+ path: '/set-enduser-password',
382
+ description: "Sets (or resets) an enduser's password. Minimum length 8 characters.",
383
+ parameters: {
384
+ id: { validator: mongoIdStringValidator, required: true },
385
+ password: { validator: stringValidator100, required: true },
386
+ },
387
+ returns: { } //authToken: { validator: stringValidator5000 } },
388
+ },
389
+ is_authenticated: {
390
+ op: "custom", access: 'read', method: "get",
391
+ name: 'Check enduser authentication',
392
+ path: '/enduser-is-authenticated',
393
+ description: "Checks the validity of an enduser's authToken",
394
+ parameters: {
395
+ id: { validator: mongoIdStringValidator, required: true },
396
+ authToken: { validator: stringValidator5000, required: true },
397
+ },
398
+ returns: {
399
+ isAuthenticated: { validator: booleanValidator, required: true },
400
+ enduser: { validator: 'enduser' },
401
+ } as any // todo: add enduser eventually, when validator defined
402
+ },
403
+ refresh_session: {
404
+ op: "custom", access: 'update', method: "post",
405
+ name: 'Refresh enduser authentication',
406
+ path: '/refresh-enduser-session',
407
+ description: "When called by an authenticated enduser, generates a new session",
408
+ parameters: { },
409
+ enduserOnly: true,
410
+ returns: {
411
+ authToken: { validator: stringValidator, required: true },
412
+ enduser: { validator: 'enduser' },
413
+ } as any // todo: add enduser eventually, when validator defined
414
+ },
415
+ logout: {
416
+ op: "custom", access: 'update', method: "post",
417
+ name: 'Logout enduser',
418
+ path: '/logout-enduser',
419
+ description: "Logs out an enduser",
420
+ parameters: {},
421
+ returns: {},
422
+ },
423
+ },
424
+ publicActions: {
425
+ login: {
426
+ op: "custom", access: 'read', method: "post",
427
+ name: 'Login enduser',
428
+ path: '/login-enduser',
429
+ description: "Generates an authentication token for access to enduser-facing endpoints",
430
+ enduserOnly: true, // implemented as authenticate in enduser sdk only
431
+ parameters: {
432
+ id: { validator: mongoIdStringValidator },
433
+ password: { validator: stringValidator100, required: true }, // required until optional challenge token available
434
+ phone: { validator: phoneValidator },
435
+ email: { validator: emailValidator },
436
+ },
437
+ returns: { authToken: { validator: stringValidator5000 } },
438
+ },
392
439
  },
393
- defaultActions: DEFAULT_OPERATIONS,
394
440
  },
395
441
  api_keys: {
396
442
  info: {},
@@ -608,7 +654,7 @@ export const schema: SchemaV1 = {
608
654
  validator: mongoIdStringValidator,
609
655
  examples: [PLACEHOLDER_ID],
610
656
  readonly: true,
611
- initializer: (a, s) => (s as UserSession).userId,
657
+ initializer: (a, s) => (s as UserSession).id,
612
658
  },
613
659
  subject: {
614
660
  validator: stringValidator,
@@ -728,7 +774,7 @@ export const schema: SchemaV1 = {
728
774
  businessUserId: {
729
775
  validator: mongoIdStringValidator,
730
776
  readonly: true, // default to only self-sending, for now
731
- initializer: (a, s) => (s as UserSession).userId,
777
+ initializer: (a, s) => (s as UserSession).id,
732
778
  dependencies: [{
733
779
  dependsOn: ['users'],
734
780
  dependencyField: '_id',
@@ -845,7 +891,7 @@ export const schema: SchemaV1 = {
845
891
  senderId: {
846
892
  validator: mongoIdStringValidator,
847
893
  readonly: true, // create a separate endpoint for storing enduser chats
848
- initializer: (a, s) => (s as UserSession).userId ?? (s as EnduserSession).enduserId,
894
+ initializer: (a, s) => (s as UserSession).id ?? (s as EnduserSession).id,
849
895
  examples: [PLACEHOLDER_ID],
850
896
  dependencies: [{ // can be userId or enduserId
851
897
  dependsOn: ['users', 'endusers'],
@@ -939,6 +985,17 @@ export const schema: SchemaV1 = {
939
985
  roles: {
940
986
  validator: listOfStringsValidator,
941
987
  },
988
+ avatar: {
989
+ validator: stringValidator100,
990
+ dependencies: [
991
+ {
992
+ dependsOn: ['files'],
993
+ dependencyField: 'secureName',
994
+ relationship: 'foreignKey',
995
+ onDependencyDelete: 'unset',
996
+ },
997
+ ]
998
+ },
942
999
  }
943
1000
  },
944
1001
  templates: {
@@ -971,5 +1028,72 @@ export const schema: SchemaV1 = {
971
1028
  initializer: () => 'enduser'
972
1029
  },
973
1030
  }
1031
+ },
1032
+ files: {
1033
+ info: {},
1034
+ constraints: { unique: [], relationship: [] },
1035
+ defaultActions: { read: {}, readMany: {}, update: {} },
1036
+ fields: {
1037
+ ...BuiltInFields,
1038
+ name: {
1039
+ validator: stringValidator250,
1040
+ required: true,
1041
+ },
1042
+ size: {
1043
+ validator: fileSizeValidator,
1044
+ required: true,
1045
+ },
1046
+ type: {
1047
+ validator: fileTypeValidator,
1048
+ required: true
1049
+ },
1050
+ secureName: {
1051
+ validator: stringValidator250,
1052
+ readonly: true,
1053
+ },
1054
+ },
1055
+ enduserActions: { prepare_file_upload: {} },
1056
+ customActions: {
1057
+ prepare_file_upload: {
1058
+ op: "custom", access: 'create', method: "post",
1059
+ name: 'Prepare File Upload',
1060
+ path: '/prepare-file-upload',
1061
+ description: "Generates an upload link for a file, storing metadata as a File record.",
1062
+ parameters: {
1063
+ name: {
1064
+ validator: stringValidator250,
1065
+ required: true,
1066
+ },
1067
+ size: {
1068
+ validator: fileSizeValidator,
1069
+ required: true,
1070
+ },
1071
+ type: {
1072
+ validator: fileTypeValidator,
1073
+ required: true
1074
+ },
1075
+ },
1076
+ returns: {
1077
+ presignedUpload: {
1078
+ validator: objectAnyFieldsValidator,
1079
+ },
1080
+ file: {
1081
+ validator: 'file' as any, // todo: add file validator
1082
+ },
1083
+ },
1084
+ },
1085
+ file_download_URL: {
1086
+ op: "custom", access: 'read', method: "get",
1087
+ name: 'Generate File Download',
1088
+ path: '/file-download-link',
1089
+ description: "Generates a temporary download link for a file.",
1090
+ parameters: {
1091
+ secureName: { validator: stringValidator250 },
1092
+ },
1093
+ returns: {
1094
+ downloadURL: { validator: stringValidator250 },
1095
+ },
1096
+ },
1097
+ },
974
1098
  }
975
1099
  }
package/tsconfig.json CHANGED
@@ -9,6 +9,6 @@
9
9
  "references": [
10
10
  { "path": "../constants" },
11
11
  { "path": "../validation" },
12
- { "path": "../types" }
12
+ { "path": "../types-server" }
13
13
  ]
14
14
  }