@liminalfunctions/framework 1.0.51 → 1.0.53

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 (48) hide show
  1. package/dist/F_Collection.d.ts +2 -0
  2. package/dist/F_Collection.js +9 -0
  3. package/dist/F_Collection.js.map +1 -1
  4. package/dist/F_Compile.js +247 -0
  5. package/dist/F_Compile.js.map +1 -1
  6. package/dist/F_Security_Models/F_SM_Role_Membership.d.ts +2 -2
  7. package/dist/F_Security_Models/F_SM_Role_Membership.js +10 -6
  8. package/dist/F_Security_Models/F_SM_Role_Membership.js.map +1 -1
  9. package/dist/code_generation/generate_client_library.d.ts +1 -0
  10. package/dist/code_generation/generate_client_library.js +37 -16
  11. package/dist/code_generation/generate_client_library.js.map +1 -1
  12. package/dist/code_generation/templates/collection.mustache +79 -1
  13. package/dist/utils/array_children_from_zod.d.ts +3 -0
  14. package/dist/utils/array_children_from_zod.js +39 -0
  15. package/dist/utils/array_children_from_zod.js.map +1 -0
  16. package/dist/utils/mongoose_from_zod.js +7 -2
  17. package/dist/utils/mongoose_from_zod.js.map +1 -1
  18. package/package.json +1 -1
  19. package/src/F_Collection.ts +14 -3
  20. package/src/F_Compile.ts +288 -16
  21. package/src/F_Security_Models/F_SM_Role_Membership.ts +12 -8
  22. package/src/code_generation/generate_client_library.ts +35 -8
  23. package/src/code_generation/templates/collection.mustache +79 -1
  24. package/src/utils/array_children_from_zod.ts +44 -0
  25. package/src/utils/mongoose_from_zod.ts +7 -3
  26. package/test/0_1_mongoose_from_zod.test.ts +16 -10
  27. package/test/0_6_array_children.test.ts +127 -0
  28. package/test/1_0_basic_server.test.ts +107 -2
  29. package/test/1_1_security_ownership.test.ts +186 -6
  30. package/test/1_2_role_membership.test.ts +329 -9
  31. package/test/2_1_client_library_generation.test.ts +125 -1
  32. package/test/tmp/dist/Brief_News_Category.js.map +1 -1
  33. package/test/tmp/dist/Client.js.map +1 -1
  34. package/test/tmp/dist/Institution.js.map +1 -1
  35. package/test/tmp/dist/Project.d.ts +20 -0
  36. package/test/tmp/dist/Project.js +63 -0
  37. package/test/tmp/dist/Project.js.map +1 -1
  38. package/test/tmp/dist/types/project.d.ts +4 -0
  39. package/test/tmp/dist/types/project_post.d.ts +4 -0
  40. package/test/tmp/dist/types/project_put.d.ts +4 -0
  41. package/test/tmp/package-lock.json +111 -111
  42. package/test/tmp/src/Brief_News_Category.ts +3 -1
  43. package/test/tmp/src/Client.ts +3 -1
  44. package/test/tmp/src/Institution.ts +3 -1
  45. package/test/tmp/src/Project.ts +73 -1
  46. package/test/tmp/src/types/project.ts +4 -0
  47. package/test/tmp/src/types/project_post.ts +4 -0
  48. package/test/tmp/src/types/project_put.ts +4 -0
@@ -26,9 +26,18 @@ function build_path(...args: string[]){
26
26
  export async function generate_client_library<Collections>(output_path: string, collection_registry: F_Collection_Registry<Collections>, service_name = 'default-service') {
27
27
  let api_builder: api_builder = {
28
28
  mustache_context: {},
29
- children: {}
29
+ children: {},
30
30
  };
31
31
 
32
+ if(!existsSync(build_path(output_path, 'src'))){ await mkdir(build_path(output_path, 'src')); }
33
+ if(!existsSync(build_path(output_path, 'dist'))){ await mkdir(build_path(output_path, 'dist')); }
34
+ if(!existsSync(build_path(output_path, 'src', 'types'))){ await mkdir(build_path(output_path, 'src', 'types')); }
35
+ if(!existsSync(build_path(output_path, 'src', 'utils'))){ await mkdir(build_path(output_path, 'src', 'utils')); }
36
+ await writeFile(build_path(output_path, 'src', 'utils', 'utils.ts'), await readFile(fileURLToPath(import.meta.resolve('./templates/utils.ts.mustache')), { encoding: 'utf-8' }));
37
+ await writeFile(build_path(output_path, 'tsconfig.json'), await readFile(fileURLToPath(import.meta.resolve('./templates/tsconfig.json.mustache')), { encoding: 'utf-8' }));
38
+ await writeFile(build_path(output_path, '.gitignore'), await readFile(fileURLToPath(import.meta.resolve('./templates/.gitignore.mustache')), { encoding: 'utf-8' }));
39
+
40
+
32
41
  // build the typescript types
33
42
  for(let col of Object.values(collection_registry.collections)){
34
43
  let collection = col as F_Collection<string, any>;
@@ -48,6 +57,27 @@ export async function generate_client_library<Collections>(output_path: string,
48
57
 
49
58
  type_post: `${get_type_name(collection.collection_id)}_post`,
50
59
  path_type_post: `types/${get_type_name(collection.collection_id)}_post`,
60
+
61
+ array_children: [] as any[],
62
+ has_array_children: false,
63
+ }
64
+
65
+ for(let [array_child_key, array_child_validator] of collection.array_children_map.entries()){
66
+ let array_child_put_type = type_from_zod(array_child_validator);
67
+ let array_child_post_type = type_from_zod(collection.array_children_post_map.get(array_child_key));
68
+ let type_name = get_array_child_type_name(mustache_context.type_return, array_child_key);
69
+ let type_put_name = `${type_name}_put`
70
+ let type_post_name = `${type_name}_post`
71
+
72
+ let array_child_mustache_context = {
73
+ array_name: array_child_key,
74
+ type_array_child_put: type_put_name,
75
+ array_type_put_definition: `export type ${type_put_name} = ${array_child_put_type[0]}\n${array_child_put_type.slice(1)}`,
76
+ type_array_child_post: type_post_name,
77
+ array_type_post_definition: `export type ${type_post_name} = ${array_child_post_type[0]}\n${array_child_post_type.slice(1)}`,
78
+ };
79
+ mustache_context.array_children.push(array_child_mustache_context);
80
+ mustache_context.has_array_children = true;
51
81
  }
52
82
 
53
83
  let collection_type_definition_builder = [] as string[];
@@ -66,13 +96,6 @@ export async function generate_client_library<Collections>(output_path: string,
66
96
  let collection_type_post = type_from_zod(collection.post_validator);
67
97
  collection_post_type_definition_builder.push(`export type ${mustache_context.type_post} = ${collection_type_post[0]}`, ...collection_type_post.slice(1));
68
98
 
69
- if(!existsSync(build_path(output_path, 'src'))){ await mkdir(build_path(output_path, 'src')); }
70
- if(!existsSync(build_path(output_path, 'dist'))){ await mkdir(build_path(output_path, 'dist')); }
71
- if(!existsSync(build_path(output_path, 'src', 'types'))){ await mkdir(build_path(output_path, 'src', 'types')); }
72
- if(!existsSync(build_path(output_path, 'src', 'utils'))){ await mkdir(build_path(output_path, 'src', 'utils')); }
73
- await writeFile(build_path(output_path, 'src', 'utils', 'utils.ts'), await readFile(fileURLToPath(import.meta.resolve('./templates/utils.ts.mustache')), { encoding: 'utf-8' }));
74
- await writeFile(build_path(output_path, 'tsconfig.json'), await readFile(fileURLToPath(import.meta.resolve('./templates/tsconfig.json.mustache')), { encoding: 'utf-8' }));
75
- await writeFile(build_path(output_path, '.gitignore'), await readFile(fileURLToPath(import.meta.resolve('./templates/.gitignore.mustache')), { encoding: 'utf-8' }));
76
99
  await writeFile(build_path(output_path, 'src', mustache_context.path_type_return + '.ts'), collection_type_definition_builder.join('\n'));
77
100
  await writeFile(build_path(output_path, 'src', mustache_context.path_type_query + '.ts'), collection_query_type_definition_builder.join('\n'));
78
101
  await writeFile(build_path(output_path, 'src', mustache_context.path_type_put + '.ts'), collection_put_type_definition_builder.join('\n'));
@@ -155,6 +178,10 @@ export function get_type_name(collection_id: string, suffix?: string): string {
155
178
  return suffix ? `${collection_id}_${suffix}`.replace(/[^(a-zA-Z0-9\_)]/g, '_') : collection_id.replace(/[^(a-zA-Z0-9\_)]/g, '_');
156
179
  }
157
180
 
181
+ export function get_array_child_type_name(collection_id: string, array_key: string): string {
182
+ return collection_id + '_' + array_key.replace(/[^(a-zA-Z0-9\_)]/g, '_');
183
+ }
184
+
158
185
  export function uppercase(str: string): string {
159
186
  let upper_on = true;
160
187
  let joinable = [];
@@ -130,4 +130,82 @@ class Document {
130
130
  }
131
131
  }
132
132
  {{/has_subcollections}}
133
- }
133
+
134
+ {{#has_array_children}}
135
+ {{#array_children}}
136
+ array(key: "{{array_name}}"): Collection_{{my_built_collection}}_Array_{{array_name}};
137
+ {{/array_children}}
138
+ array(key: string) {
139
+ switch(key) {
140
+ {{#array_children}}
141
+ case "{{array_name}}":
142
+ return new Collection_{{my_built_collection}}_Array_{{array_name}}([...this.path, this.document_id, "{{array_name}}"], this.get_auth);
143
+ {{/array_children}}
144
+ default:
145
+ throw new Error(`Collection ${this.collection_id} does not have an array at the key ${key}`)
146
+ }
147
+ }
148
+ {{/has_array_children}}
149
+ }
150
+
151
+ {{#array_children}}
152
+ {{array_type_put_definition}}
153
+ {{array_type_post_definition}}
154
+
155
+ export class Collection_{{my_built_collection}}_Array_{{array_name}} {
156
+ path: string[]
157
+ get_auth: () => Promise<any>
158
+ collection_id: string
159
+ collection_name_plural: string
160
+ array_key: string
161
+
162
+ constructor(path: string[], get_auth: () => Promise<any>) {
163
+ this.path = path;
164
+ this.get_auth = get_auth;
165
+ this.collection_id = "{{collection_id}}";
166
+ this.collection_name_plural = "{{collection_name_plural}}"
167
+ this.array_key = "{{array_name}}"
168
+ }
169
+
170
+ async push(document: {{type_array_child_post}}): Promise<{{type_return}}>{
171
+ try {
172
+ let result = await ky.post(this.path.join('/'), {
173
+ headers: {
174
+ authorization: await this.get_auth()
175
+ },
176
+ json: document
177
+ }).json() as Response<{{type_return}}>;
178
+ return result.data;
179
+ } catch(err){
180
+ return Promise.reject(err)
181
+ }
182
+ }
183
+
184
+ async replace(document: {{type_array_child_put}}): Promise<{{type_return}}>{
185
+ try {
186
+ let result = await ky.put([...this.path, document._id].join('/'), {
187
+ headers: {
188
+ authorization: await this.get_auth()
189
+ },
190
+ json: document
191
+ }).json() as Response<{{type_return}}>;
192
+ return result.data;
193
+ } catch(err){
194
+ return Promise.reject(err)
195
+ }
196
+ }
197
+
198
+ async delete(document_id: string): Promise<{{type_return}}>{
199
+ try {
200
+ let result = await ky.delete([...this.path, document_id].join('/'), {
201
+ headers: {
202
+ authorization: await this.get_auth()
203
+ }
204
+ }).json() as Response<{{type_return}}>;
205
+ return result.data;
206
+ } catch(err){
207
+ return Promise.reject(err)
208
+ }
209
+ }
210
+ }
211
+ {{/array_children}}
@@ -0,0 +1,44 @@
1
+ import z from "zod/v4";
2
+ import { find_loops, validator_group } from './zod_loop_seperator.js'
3
+
4
+ export function array_children_from_zod(zod_definition: z.ZodObject, loop_detector?: Map<any, validator_group>, built_map?: Map<string, z.ZodObject>, prefix: string = ''): Map<string, z.ZodObject> {
5
+ let loops = loop_detector ?? find_loops(zod_definition as z.ZodType);
6
+ let results = built_map ?? new Map<string, z.ZodObject>();
7
+
8
+ for(let [key, value] of Object.entries(zod_definition.shape)){
9
+ if(loops.has(value._zod.def)){ continue; }
10
+ let real_value = distill_zod(value);
11
+ switch (real_value._zod.def.type) {
12
+ case "object":
13
+ array_children_from_zod(real_value as z.ZodObject, loop_detector, results, prefix.length > 0 ? `${prefix}.${key}` : key)
14
+ break;
15
+ case "array":
16
+ //@ts-ignore
17
+ let element = distill_zod((real_value as z.ZodArray).element);
18
+ if(element._zod.def.type === 'object') {
19
+ let objdef = element._zod.def as z.core.$ZodObjectDef;
20
+ if(objdef.shape._id){
21
+ results.set(prefix.length > 0 ? `${prefix}.${key}` : key, element as z.ZodObject);
22
+ }
23
+ }
24
+ break;
25
+ default:
26
+ break;
27
+ }
28
+
29
+ }
30
+ return results;
31
+ }
32
+
33
+ function distill_zod(zod_definition: z.ZodType): z.ZodType {
34
+ switch (zod_definition._zod.def.type) {
35
+ case "nullable":
36
+ //@ts-ignore
37
+ return zod_definition._zod.def.innerType;
38
+ case "optional":
39
+ //@ts-ignore
40
+ return zod_definition._zod.def.innerType;
41
+ default:
42
+ return zod_definition;
43
+ }
44
+ }
@@ -100,10 +100,10 @@ export function schema_entry_from_zod(zod_definition: z.ZodType, loop_detector:
100
100
  result.required = !zod_definition.safeParse(undefined).success
101
101
  return result;
102
102
  case "nullable":
103
- // stuff is nullable in mongodb by default, so just return the ordinary results of the parse
104
- //@ts-expect-error
105
- return schema_entry_from_zod((zod_definition as z.core.$ZodNullable)._zod.def.innerType, loop_detector)
106
103
  case "optional":
104
+ // stuff is nullable in mongodb by default, so just return the ordinary results of the parse
105
+ ////@ts-expect-error
106
+ //return schema_entry_from_zod((zod_definition as z.core.$ZodNullable)._zod.def.innerType, loop_detector)
107
107
  return parse_optional(zod_definition._zod.def as z.core.$ZodOptionalDef, loop_detector);
108
108
  case "record":
109
109
  result = parse_record(zod_definition._zod.def as z.core.$ZodRecordDef, loop_detector);
@@ -171,6 +171,10 @@ function parse_object(def: z.core.$ZodObjectDef, loop_detector: Map<any, validat
171
171
  //@ts-ignore
172
172
  retval[key] = schema_entry_from_zod(value, loop_detector);
173
173
  }
174
+
175
+ // handle the edge cases arouund mongoose's auto-IDs
176
+ if(!retval._id){ retval._id = false; }
177
+ else { delete retval._id; }
174
178
  return {mongoose_type: retval, required: true};
175
179
  }
176
180
 
@@ -69,7 +69,7 @@ describe('Mongoose from Zod', function () {
69
69
  it(`should convert ${basic_type.label} to mongoose type with nullable`, function () {
70
70
  let zodSchema = z.object({ test_value: basic_type.zod_function().nullable() })
71
71
  let mongooseSchema = schema_from_zod(zodSchema)
72
- assert.deepEqual({ test_value: { mongoose_type: basic_type.mongoose_type, required: true } }, mongooseSchema)
72
+ assert.deepEqual({ test_value: { mongoose_type: basic_type.mongoose_type, required: false } }, mongooseSchema)
73
73
  });
74
74
 
75
75
  it(`should convert ${basic_type.label} to mongoose type with optional AND default values`, function () {
@@ -87,20 +87,26 @@ describe('Mongoose from Zod', function () {
87
87
  it(`should convert a nested object containing a ${basic_type.label} property to mongoose type`, function () {
88
88
  let zodSchema = z.object({ test_value: z.object({test_value: basic_type.zod_function() }) })
89
89
  let mongooseSchema = schema_from_zod(zodSchema)
90
- assert.deepEqual({ test_value: { mongoose_type: {test_value: { mongoose_type: basic_type.mongoose_type, required: true }}, required: true} }, mongooseSchema)
90
+ assert.deepEqual({ test_value: { mongoose_type: { _id: false, test_value: { mongoose_type: basic_type.mongoose_type, required: true }}, required: true} }, mongooseSchema)
91
91
  });
92
92
 
93
93
  it(`should convert a nested object containing a ${basic_type.label} property to mongoose type with optional`, function () {
94
94
  let zodSchema = z.object({ test_value: z.object({test_value: basic_type.zod_function().optional() }) })
95
95
  let mongooseSchema = schema_from_zod(zodSchema)
96
- assert.deepEqual({ test_value: { mongoose_type: {test_value: { mongoose_type: basic_type.mongoose_type, required: false }}, required: true } }, mongooseSchema)
96
+ assert.deepEqual({ test_value: { mongoose_type: {_id: false, test_value: { mongoose_type: basic_type.mongoose_type, required: false }}, required: true } }, mongooseSchema)
97
97
  });
98
98
 
99
99
  it(`should convert a nested object containing a ${basic_type.label} property to mongoose type with default values`, function () {
100
100
  //@ts-ignore
101
101
  let zodSchema = z.object({ test_value: z.object({test_value: basic_type.zod_function().default(basic_type.default_val) }) })
102
102
  let mongooseSchema = schema_from_zod(zodSchema)
103
- assert.deepEqual({ test_value: { mongoose_type: {test_value: { mongoose_type: basic_type.mongoose_type, required: true, default: basic_type.default_val }}, required: true } }, mongooseSchema)
103
+ assert.deepEqual({ test_value: { mongoose_type: {_id: false, test_value: { mongoose_type: basic_type.mongoose_type, required: true, default: basic_type.default_val }}, required: true } }, mongooseSchema)
104
+ });
105
+
106
+ it(`should convert a nested object containing a ${basic_type.label} property to mongoose type while allowing for auto-ID`, function () {
107
+ let zodSchema = z.object({ test_value: z.object({_id: z_mongodb_id_optional, test_value: basic_type.zod_function() }) })
108
+ let mongooseSchema = schema_from_zod(zodSchema)
109
+ assert.deepEqual({ test_value: { mongoose_type: {test_value: { mongoose_type: basic_type.mongoose_type, required: true }}, required: true} }, mongooseSchema)
104
110
  });
105
111
  }
106
112
 
@@ -268,14 +274,14 @@ describe('Mongoose from Zod', function () {
268
274
  c: z.object({
269
275
  name: z.string()
270
276
  }),
271
- })
272
- let mongooseSchema = schema_from_zod(zodSchema)
277
+ });
278
+ let mongooseSchema = schema_from_zod(zodSchema);
273
279
 
274
280
  assert.deepEqual({
275
- a: { mongoose_type: { name: { mongoose_type: String, required: true } }, required: true },
276
- b: { mongoose_type: { name: { mongoose_type: String, required: true } }, required: true },
277
- c: { mongoose_type: { name: { mongoose_type: String, required: true } }, required: true },
278
- }, mongooseSchema)
281
+ a: { mongoose_type: { _id: false, name: { mongoose_type: String, required: true } }, required: true },
282
+ b: { mongoose_type: { _id: false, name: { mongoose_type: String, required: true } }, required: true },
283
+ c: { mongoose_type: { _id: false, name: { mongoose_type: String, required: true } }, required: true },
284
+ }, mongooseSchema);
279
285
  })
280
286
 
281
287
  /*
@@ -0,0 +1,127 @@
1
+ import assert from "assert";
2
+ import { z, ZodBoolean, ZodDate, ZodNumber, ZodString } from 'zod'
3
+
4
+ import { array_children_from_zod } from '../dist/utils/array_children_from_zod.js';
5
+ import mongoose, { Schema } from 'mongoose'
6
+ import { required } from "zod/mini";
7
+ import { z_mongodb_id } from "../dist/utils/mongoose_from_zod.js";
8
+
9
+ process.env.DEBUG = 'express:*'
10
+
11
+ describe('Mongoose from Zod', function () {
12
+ it('should detect no loops in an empty object', function () {
13
+ let zodSchema = z.object({ })
14
+ let array_children = array_children_from_zod(zodSchema);
15
+ assert.equal(array_children.size, 0)
16
+ });
17
+
18
+ it('should detect an array child if one exists', function () {
19
+ let zodSchema = z.object({
20
+ val: z.array(z.object({
21
+ _id: z_mongodb_id,
22
+ test: z.string()
23
+ }))
24
+ })
25
+ let array_children = array_children_from_zod(zodSchema);
26
+ assert.equal(array_children.size, 1)
27
+ });
28
+
29
+ it('should detect multiple array children if they exist', function () {
30
+
31
+ let zodSchema = z.object({
32
+ val: z.array(z.object({
33
+ _id: z_mongodb_id,
34
+ test: z.string()
35
+ })),
36
+ other_val: z.array(z.object({
37
+ _id: z_mongodb_id,
38
+ test: z.string()
39
+ }))
40
+ })
41
+ let array_children = array_children_from_zod(zodSchema);
42
+ assert.equal(array_children.size, 2)
43
+ });
44
+
45
+ it('should detect array children within nullable', function () {
46
+ let zodSchema = z.object({
47
+ val: z.nullable(z.array(z.object({
48
+ _id: z_mongodb_id,
49
+ test: z.string()
50
+ })))
51
+ })
52
+ let array_children = array_children_from_zod(zodSchema);
53
+ assert.equal(array_children.size, 1)
54
+ });
55
+
56
+ it('should detect array children within optional', function () {
57
+ let zodSchema = z.object({
58
+ val: z.optional(z.array(z.object({
59
+ _id: z_mongodb_id,
60
+ test: z.string()
61
+ })))
62
+ })
63
+ let array_children = array_children_from_zod(zodSchema);
64
+ assert.equal(array_children.size, 1)
65
+ });
66
+
67
+ it('should detect array children nested within an object', function () {
68
+ let zodSchema = z.object({
69
+ alpha: z.object({
70
+ val: z.array(z.object({
71
+ _id: z_mongodb_id,
72
+ test: z.string()
73
+ }))
74
+ })
75
+ })
76
+ let array_children = array_children_from_zod(zodSchema);
77
+ assert.equal(array_children.size, 1)
78
+ });
79
+
80
+ it('should have correct array children and nested array children names', function () {
81
+ let zodSchema = z.object({
82
+ alpha: z.object({
83
+ val: z.array(z.object({
84
+ _id: z_mongodb_id,
85
+ test: z.string()
86
+ }))
87
+ }),
88
+ latex: z.array(z.object({
89
+ _id: z_mongodb_id,
90
+ test: z.string()
91
+ }))
92
+ })
93
+ let array_children = array_children_from_zod(zodSchema);
94
+ assert.equal(Array.from(array_children.keys()).includes('alpha.val'), true);
95
+ assert.equal(Array.from(array_children.keys()).includes('latex'), true);
96
+ });
97
+
98
+ it('should return the validator for a child', function () {
99
+ let zodSchema = z.object({
100
+ val: z.array(z.object({
101
+ _id: z_mongodb_id,
102
+ test: z.string()
103
+ }))
104
+ })
105
+ let array_children = array_children_from_zod(zodSchema);
106
+ let objectid = new mongoose.Types.ObjectId()
107
+ assert.deepEqual(array_children.get('val')?.parse({_id: '' + objectid, test: 'testval'}), {_id: '' + objectid, test: 'testval'})
108
+ });
109
+
110
+
111
+ it('should only detect array children that have an _id field', function () {
112
+ let zodSchema = z.object({
113
+ alpha: z.object({
114
+ val: z.array(z.object({
115
+ _id: z_mongodb_id,
116
+ test: z.string()
117
+ }))
118
+ }),
119
+ latex: z.array(z.object({
120
+ test: z.string()
121
+ }))
122
+ })
123
+ let array_children = array_children_from_zod(zodSchema);
124
+ assert.equal(Array.from(array_children.keys()).includes('alpha.val'), true);
125
+ assert.equal(Array.from(array_children.keys()).includes('latex'), false);
126
+ });
127
+ });
@@ -1,7 +1,7 @@
1
1
 
2
2
  import assert from "assert";
3
3
 
4
- import { z_mongodb_id } from '../dist/utils/mongoose_from_zod.js';
4
+ import { z_mongodb_id, z_mongodb_id_optional } from '../dist/utils/mongoose_from_zod.js';
5
5
  import { F_Collection } from '../dist/f_collection.js';
6
6
  import { F_Collection_Registry } from '../dist/F_Collection_Registry.js'
7
7
  import { F_SM_Open_Access } from '../dist/F_Security_Models/F_SM_Open_Access.js'
@@ -41,10 +41,20 @@ describe('Basic Server', function () {
41
41
  client_id: z_mongodb_id,
42
42
  name: z.string(),
43
43
  });
44
+ const validate_list_container = z.object({
45
+ _id: z_mongodb_id,
46
+ container: z.object({
47
+ list: z.array(z.object({
48
+ _id: z_mongodb_id_optional,
49
+ value: z.string()
50
+ }))
51
+ })
52
+ });
44
53
 
45
54
  let institution: F_Collection<'institution', typeof validate_institution>;
46
55
  let client: F_Collection<'client', typeof validate_client>;
47
56
  let project: F_Collection<'project', typeof validate_project>;
57
+ let list_container: F_Collection<'list_container', typeof validate_list_container>;
48
58
 
49
59
  let registry: F_Collection_Registry;
50
60
 
@@ -67,9 +77,12 @@ describe('Basic Server', function () {
67
77
  project = new F_Collection('project', 'projects', validate_project);
68
78
  project.add_layers([institution.collection_id, client.collection_id], [new F_SM_Open_Access(project)]);
69
79
 
80
+ list_container = new F_Collection('list_container', 'list_containers', validate_list_container);
81
+ list_container.add_layers([], [new F_SM_Open_Access(list_container)]);
82
+
70
83
  // build registry
71
84
  let proto_registry = new F_Collection_Registry();
72
- registry = proto_registry.register(institution).register(client).register(project);
85
+ registry = proto_registry.register(institution).register(client).register(project).register(list_container);
73
86
  registry.compile(express_app, '/api');
74
87
 
75
88
  server = express_app.listen(port);
@@ -563,6 +576,40 @@ describe('Basic Server', function () {
563
576
  });
564
577
  });
565
578
 
579
+ it(`should reject a PUT operation that changes the document id`, async function () {
580
+ let test_institution = await institution.mongoose_model.create({
581
+ name: 'Spandex Co'
582
+ });
583
+
584
+ let test_client = await client.mongoose_model.create({
585
+ institution_id: test_institution._id,
586
+ name: `Bob's spandex house`
587
+ })
588
+
589
+ let test_project = await project.mongoose_model.create({
590
+ institution_id: test_institution._id,
591
+ client_id: test_client._id,
592
+ name: `Spandex Reincarnation`
593
+ })
594
+
595
+ let test_project_2 = await project.mongoose_model.create({
596
+ institution_id: test_institution._id,
597
+ client_id: test_client._id,
598
+ name: `Olfactory Hurricane`
599
+ })
600
+
601
+ await assert.rejects(async () => {
602
+ let results = await got.put(`http://localhost:${port}/api/institution/${test_institution._id}/client/${test_client._id}/project/${test_project._id}`, {
603
+ json: {
604
+ _id: test_project_2._id,
605
+ name: `Leather Pants Transubstantiation`,
606
+ },
607
+ }).json();
608
+ }, {
609
+ message: 'Response code 400 (Bad Request)'
610
+ });
611
+ });
612
+
566
613
  it(`should reject a PUT operation with a malicious key in the body`, async function () {
567
614
  let test_institution = await institution.mongoose_model.create({
568
615
  name: 'Spandex Co'
@@ -769,4 +816,62 @@ describe('Basic Server', function () {
769
816
  assert.deepEqual(null, results.data);
770
817
  assert.deepEqual(JSON.parse(JSON.stringify(await project.mongoose_model.findById(test_project._id))), JSON.parse(JSON.stringify(test_project)));
771
818
  });
819
+
820
+ it(`should allow entries to be added to object arrays`, async function () {
821
+ let test_list_container = await list_container.mongoose_model.create({
822
+ container: {
823
+ list: []
824
+ }
825
+ });
826
+
827
+ let results = await got.post(`http://localhost:${port}/api/list_container/${test_list_container._id}/container.list`, {
828
+ json: {
829
+ value: 'test value'
830
+ }
831
+ }).json();
832
+
833
+ //@ts-ignore
834
+ assert.deepEqual('test value', results.data.container.list[0].value);
835
+ //@ts-ignore
836
+ assert.deepEqual(JSON.parse(JSON.stringify(await list_container.mongoose_model.findById(test_list_container._id))), JSON.parse(JSON.stringify(results.data)));
837
+ });
838
+
839
+ it(`should allow object array entries to be updated`, async function () {
840
+ let test_list_container = await list_container.mongoose_model.create({
841
+ container: {
842
+ list: [{
843
+ value: 'original value'
844
+ }]
845
+ }
846
+ });
847
+
848
+ let results = await got.put(`http://localhost:${port}/api/list_container/${test_list_container._id}/container.list/${test_list_container.container.list[0]._id}`, {
849
+ json: {
850
+ value: 'updated value'
851
+ }
852
+ }).json();
853
+
854
+ //@ts-ignore
855
+ assert.deepEqual('updated value', results.data.container.list[0].value);
856
+ //@ts-ignore
857
+ assert.deepEqual(JSON.parse(JSON.stringify(await list_container.mongoose_model.findById(test_list_container._id))), JSON.parse(JSON.stringify(results.data)));
858
+ });
859
+
860
+ it(`should allow object array entries to be deleted`, async function () {
861
+ let test_list_container = await list_container.mongoose_model.create({
862
+ container: {
863
+ list: [{
864
+ value: 'original value'
865
+ }]
866
+ }
867
+ });
868
+
869
+ let results = await got.delete(`http://localhost:${port}/api/list_container/${test_list_container._id}/container.list/${test_list_container.container.list[0]._id}`).json();
870
+
871
+ //@ts-ignore
872
+ assert.deepEqual(0, results.data.container.list.length);
873
+ //@ts-ignore
874
+ assert.deepEqual(JSON.parse(JSON.stringify(await list_container.mongoose_model.findById(test_list_container._id))), JSON.parse(JSON.stringify(results.data)));
875
+ });
876
+
772
877
  });