@liminalfunctions/framework 1.0.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.
Files changed (181) hide show
  1. package/.mocharc.json +5 -0
  2. package/dist/F_Client_Collection_Registry.d.ts +18 -0
  3. package/dist/F_Client_Collection_Registry.js +36 -0
  4. package/dist/F_Client_Collection_Registry.js.map +1 -0
  5. package/dist/F_Collection.d.ts +21 -0
  6. package/dist/F_Collection.js +36 -0
  7. package/dist/F_Collection.js.map +1 -0
  8. package/dist/F_Collection_Registry.d.ts +11 -0
  9. package/dist/F_Collection_Registry.js +18 -0
  10. package/dist/F_Collection_Registry.js.map +1 -0
  11. package/dist/F_Compile.d.ts +4 -0
  12. package/dist/F_Compile.js +298 -0
  13. package/dist/F_Compile.js.map +1 -0
  14. package/dist/F_Security_Models/F_SM_Open_Access.d.ts +11 -0
  15. package/dist/F_Security_Models/F_SM_Open_Access.js +14 -0
  16. package/dist/F_Security_Models/F_SM_Open_Access.js.map +1 -0
  17. package/dist/F_Security_Models/F_SM_Ownership.d.ts +12 -0
  18. package/dist/F_Security_Models/F_SM_Ownership.js +46 -0
  19. package/dist/F_Security_Models/F_SM_Ownership.js.map +1 -0
  20. package/dist/F_Security_Models/F_SM_Role_Membership.d.ts +19 -0
  21. package/dist/F_Security_Models/F_SM_Role_Membership.js +73 -0
  22. package/dist/F_Security_Models/F_SM_Role_Membership.js.map +1 -0
  23. package/dist/F_Security_Models/F_Security_Model.d.ts +41 -0
  24. package/dist/F_Security_Models/F_Security_Model.js +29 -0
  25. package/dist/F_Security_Models/F_Security_Model.js.map +1 -0
  26. package/dist/code_generation/generate_client_library.d.ts +4 -0
  27. package/dist/code_generation/generate_client_library.js +158 -0
  28. package/dist/code_generation/generate_client_library.js.map +1 -0
  29. package/dist/code_generation/templates/.gitignore.mustache +383 -0
  30. package/dist/code_generation/templates/collection.mustache +106 -0
  31. package/dist/code_generation/templates/main.mustache +24 -0
  32. package/dist/code_generation/templates/package.json.mustache +18 -0
  33. package/dist/code_generation/templates/tsconfig.json.mustache +14 -0
  34. package/dist/code_generation/templates/types.mustache +4 -0
  35. package/dist/code_generation/templates/utils.ts.mustache +17 -0
  36. package/dist/code_generation/utils/tab_indent.d.ts +1 -0
  37. package/dist/code_generation/utils/tab_indent.js +4 -0
  38. package/dist/code_generation/utils/tab_indent.js.map +1 -0
  39. package/dist/code_generation/utils/type_from_zod.d.ts +2 -0
  40. package/dist/code_generation/utils/type_from_zod.js +102 -0
  41. package/dist/code_generation/utils/type_from_zod.js.map +1 -0
  42. package/dist/utils/cache.d.ts +13 -0
  43. package/dist/utils/cache.js +101 -0
  44. package/dist/utils/cache.js.map +1 -0
  45. package/dist/utils/mongoose_from_zod.d.ts +13 -0
  46. package/dist/utils/mongoose_from_zod.js +164 -0
  47. package/dist/utils/mongoose_from_zod.js.map +1 -0
  48. package/dist/utils/pretty_print_zod.d.ts +2 -0
  49. package/dist/utils/pretty_print_zod.js +63 -0
  50. package/dist/utils/pretty_print_zod.js.map +1 -0
  51. package/dist/utils/query_object_to_mongodb_query.d.ts +3 -0
  52. package/dist/utils/query_object_to_mongodb_query.js +61 -0
  53. package/dist/utils/query_object_to_mongodb_query.js.map +1 -0
  54. package/dist/utils/query_validator_from_zod.d.ts +6 -0
  55. package/dist/utils/query_validator_from_zod.js +216 -0
  56. package/dist/utils/query_validator_from_zod.js.map +1 -0
  57. package/package.json +36 -0
  58. package/src/F_Collection.ts +50 -0
  59. package/src/F_Collection_Registry.ts +29 -0
  60. package/src/F_Compile.ts +368 -0
  61. package/src/F_Security_Models/F_SM_Open_Access.ts +21 -0
  62. package/src/F_Security_Models/F_SM_Ownership.ts +72 -0
  63. package/src/F_Security_Models/F_SM_Role_Membership.ts +87 -0
  64. package/src/F_Security_Models/F_Security_Model.ts +85 -0
  65. package/src/code_generation/generate_client_library.ts +197 -0
  66. package/src/code_generation/templates/.gitignore.mustache +383 -0
  67. package/src/code_generation/templates/collection.mustache +106 -0
  68. package/src/code_generation/templates/main.mustache +24 -0
  69. package/src/code_generation/templates/package.json.mustache +18 -0
  70. package/src/code_generation/templates/tsconfig.json.mustache +14 -0
  71. package/src/code_generation/templates/types.mustache +4 -0
  72. package/src/code_generation/templates/utils.ts.mustache +17 -0
  73. package/src/code_generation/utils/tab_indent.ts +3 -0
  74. package/src/code_generation/utils/type_from_zod.ts +140 -0
  75. package/src/utils/cache.ts +149 -0
  76. package/src/utils/mongoose_from_zod.ts +191 -0
  77. package/src/utils/pretty_print_zod.ts +75 -0
  78. package/src/utils/query_object_to_mongodb_query.ts +73 -0
  79. package/src/utils/query_validator_from_zod.ts +246 -0
  80. package/test/0_0_mongoose_from_zod.test.ts +260 -0
  81. package/test/0_1_query_validator_from_zod.test.ts +518 -0
  82. package/test/0_2_query_validator_to_mongodb_query.test.ts +365 -0
  83. package/test/0_3_cache.test.ts +204 -0
  84. package/test/1_0_basic_server.test.ts +530 -0
  85. package/test/1_1_security_ownership.test.ts +328 -0
  86. package/test/1_2_role_membership.test.ts +731 -0
  87. package/test/2_0_client_library_basic_type_generation.test.ts +444 -0
  88. package/test/2_0_client_library_query_type_generation.test.ts +352 -0
  89. package/test/2_1_client_library_generation.test.ts +255 -0
  90. package/test/tmp/dist/Brief_News_Category.d.ts +16 -0
  91. package/test/tmp/dist/Brief_News_Category.js +85 -0
  92. package/test/tmp/dist/Brief_News_Category.js.map +1 -0
  93. package/test/tmp/dist/Client.d.ts +19 -0
  94. package/test/tmp/dist/Client.js +97 -0
  95. package/test/tmp/dist/Client.js.map +1 -0
  96. package/test/tmp/dist/Institution.d.ts +18 -0
  97. package/test/tmp/dist/Institution.js +94 -0
  98. package/test/tmp/dist/Institution.js.map +1 -0
  99. package/test/tmp/dist/Project.d.ts +16 -0
  100. package/test/tmp/dist/Project.js +85 -0
  101. package/test/tmp/dist/Project.js.map +1 -0
  102. package/test/tmp/dist/index.d.ts +4 -0
  103. package/test/tmp/dist/index.js +14 -0
  104. package/test/tmp/dist/index.js.map +1 -0
  105. package/test/tmp/dist/types/brief_news_category.d.ts +7 -0
  106. package/test/tmp/dist/types/brief_news_category.js +2 -0
  107. package/test/tmp/dist/types/brief_news_category.js.map +1 -0
  108. package/test/tmp/dist/types/brief_news_category_post.d.ts +7 -0
  109. package/test/tmp/dist/types/brief_news_category_post.js +2 -0
  110. package/test/tmp/dist/types/brief_news_category_post.js.map +1 -0
  111. package/test/tmp/dist/types/brief_news_category_put.d.ts +7 -0
  112. package/test/tmp/dist/types/brief_news_category_put.js +2 -0
  113. package/test/tmp/dist/types/brief_news_category_put.js.map +1 -0
  114. package/test/tmp/dist/types/brief_news_category_query.d.ts +26 -0
  115. package/test/tmp/dist/types/brief_news_category_query.js +2 -0
  116. package/test/tmp/dist/types/brief_news_category_query.js.map +1 -0
  117. package/test/tmp/dist/types/client.d.ts +5 -0
  118. package/test/tmp/dist/types/client.js +2 -0
  119. package/test/tmp/dist/types/client.js.map +1 -0
  120. package/test/tmp/dist/types/client_post.d.ts +5 -0
  121. package/test/tmp/dist/types/client_post.js +2 -0
  122. package/test/tmp/dist/types/client_post.js.map +1 -0
  123. package/test/tmp/dist/types/client_put.d.ts +5 -0
  124. package/test/tmp/dist/types/client_put.js +2 -0
  125. package/test/tmp/dist/types/client_put.js.map +1 -0
  126. package/test/tmp/dist/types/client_query.d.ts +18 -0
  127. package/test/tmp/dist/types/client_query.js +2 -0
  128. package/test/tmp/dist/types/client_query.js.map +1 -0
  129. package/test/tmp/dist/types/institution.d.ts +4 -0
  130. package/test/tmp/dist/types/institution.js +2 -0
  131. package/test/tmp/dist/types/institution.js.map +1 -0
  132. package/test/tmp/dist/types/institution_post.d.ts +4 -0
  133. package/test/tmp/dist/types/institution_post.js +2 -0
  134. package/test/tmp/dist/types/institution_post.js.map +1 -0
  135. package/test/tmp/dist/types/institution_put.d.ts +4 -0
  136. package/test/tmp/dist/types/institution_put.js +2 -0
  137. package/test/tmp/dist/types/institution_put.js.map +1 -0
  138. package/test/tmp/dist/types/institution_query.d.ts +14 -0
  139. package/test/tmp/dist/types/institution_query.js +2 -0
  140. package/test/tmp/dist/types/institution_query.js.map +1 -0
  141. package/test/tmp/dist/types/project.d.ts +7 -0
  142. package/test/tmp/dist/types/project.js +2 -0
  143. package/test/tmp/dist/types/project.js.map +1 -0
  144. package/test/tmp/dist/types/project_post.d.ts +7 -0
  145. package/test/tmp/dist/types/project_post.js +2 -0
  146. package/test/tmp/dist/types/project_post.js.map +1 -0
  147. package/test/tmp/dist/types/project_put.d.ts +7 -0
  148. package/test/tmp/dist/types/project_put.js +2 -0
  149. package/test/tmp/dist/types/project_put.js.map +1 -0
  150. package/test/tmp/dist/types/project_query.d.ts +27 -0
  151. package/test/tmp/dist/types/project_query.js +2 -0
  152. package/test/tmp/dist/types/project_query.js.map +1 -0
  153. package/test/tmp/dist/utils/utils.d.ts +11 -0
  154. package/test/tmp/dist/utils/utils.js +13 -0
  155. package/test/tmp/dist/utils/utils.js.map +1 -0
  156. package/test/tmp/package-lock.json +573 -0
  157. package/test/tmp/package.json +18 -0
  158. package/test/tmp/src/Brief_News_Category.ts +94 -0
  159. package/test/tmp/src/Client.ts +106 -0
  160. package/test/tmp/src/Institution.ts +103 -0
  161. package/test/tmp/src/Project.ts +94 -0
  162. package/test/tmp/src/index.ts +20 -0
  163. package/test/tmp/src/types/brief_news_category.ts +7 -0
  164. package/test/tmp/src/types/brief_news_category_post.ts +7 -0
  165. package/test/tmp/src/types/brief_news_category_put.ts +7 -0
  166. package/test/tmp/src/types/brief_news_category_query.ts +26 -0
  167. package/test/tmp/src/types/client.ts +5 -0
  168. package/test/tmp/src/types/client_post.ts +5 -0
  169. package/test/tmp/src/types/client_put.ts +5 -0
  170. package/test/tmp/src/types/client_query.ts +18 -0
  171. package/test/tmp/src/types/institution.ts +4 -0
  172. package/test/tmp/src/types/institution_post.ts +4 -0
  173. package/test/tmp/src/types/institution_put.ts +4 -0
  174. package/test/tmp/src/types/institution_query.ts +14 -0
  175. package/test/tmp/src/types/project.ts +7 -0
  176. package/test/tmp/src/types/project_post.ts +7 -0
  177. package/test/tmp/src/types/project_put.ts +7 -0
  178. package/test/tmp/src/types/project_query.ts +27 -0
  179. package/test/tmp/src/utils/utils.ts +17 -0
  180. package/test/tmp/tsconfig.json +14 -0
  181. package/tsconfig.json +14 -0
@@ -0,0 +1,368 @@
1
+ import * as z from "zod/v4";
2
+ import { Router, Request, Response } from "express";
3
+ import { isValidObjectId } from "mongoose";
4
+
5
+ import { F_Collection } from "./F_Collection.js";
6
+ import { F_Security_Model, Authenticated_Request } from "./F_Security_Models/F_Security_Model.js";
7
+ import { query_object_to_mongodb_limits, query_object_to_mongodb_query } from "./utils/query_object_to_mongodb_query.js";
8
+ import { z_mongodb_id } from "./utils/mongoose_from_zod.js";
9
+
10
+ export function compile<Collection_ID extends string, ZodSchema extends z.ZodObject>(app: Router, collection: F_Collection<Collection_ID, ZodSchema>, api_prefix: string){
11
+ for(let access_layers of collection.access_layers){
12
+
13
+ /*app.use((req, res, next) => {
14
+ console.log(`${req.method} ${req.originalUrl}`)
15
+ next();
16
+ })*/
17
+
18
+ let base_layers_path_components = access_layers.layers.flatMap(ele => [ele, ':' + ele]);
19
+
20
+ let get_one_path = [
21
+ api_prefix,
22
+ ...base_layers_path_components,
23
+ `${collection.collection_id}/:document_id`
24
+ ].join('/')
25
+
26
+ // get individual document
27
+ app.get(get_one_path, async (req: Request, res: Response) => {
28
+ // ensure the the document ID passed in is valid so that mongodb doesn't have a cow
29
+ if (!isValidObjectId(req.params.document_id)) {
30
+ res.status(400);
31
+ res.json({ error: `${req.params.document_id} is not a valid document ID.` });
32
+ return;
33
+ }
34
+
35
+ let find = { '_id': req.params.document_id } as { [key: string]: any }
36
+ for(let layer of access_layers.layers){
37
+ find[`${layer}_id`] = req.params[layer];
38
+ }
39
+
40
+ let permissive_security_model = await F_Security_Model.model_with_permission(access_layers.security_models, req, res, find, 'get');
41
+ if (!permissive_security_model) {
42
+ res.status(403);
43
+ res.json({ error: `You do not have permission to fetch documents from ${req.params.document_type}.` });
44
+ return;
45
+ }
46
+
47
+ let document;
48
+ try {
49
+ //@ts-expect-error
50
+ document = await collection.mongoose_model.findOne(find, undefined, { 'lean': true });
51
+ } catch(err){
52
+ res.status(500);
53
+ res.json({ error: `there was a novel error` });
54
+ console.error(err);
55
+ return;
56
+ }
57
+
58
+ if (!document) {
59
+ let sendable = await permissive_security_model.handle_empty_query_results(req, res, 'get');
60
+ res.json(sendable);
61
+ } else {
62
+ //await req.schema.handle_pre_send(req, document);
63
+ res.json({ data: document });
64
+ }
65
+ //await req.schema.fire_api_event('get', req, [document]);
66
+ })
67
+
68
+
69
+ let get_multiple_path = [
70
+ api_prefix,
71
+ ...base_layers_path_components,
72
+ collection.collection_id
73
+ ].join('/')
74
+
75
+ app.get(get_multiple_path, async (req: Request, res: Response) => {
76
+ let validated_query_args: { [key: string]: any } ;
77
+ try {
78
+ validated_query_args = collection.query_validator_server.parse(req.query);
79
+ } catch(err){
80
+ if(err instanceof z.ZodError){
81
+ res.status(400);
82
+ res.json({ error: err.issues });
83
+ return;
84
+ } else {
85
+ console.error(err);
86
+ res.status(500);
87
+ res.json({ error: `there was a novel error` });
88
+ return;
89
+ }
90
+ }
91
+
92
+ let find = query_object_to_mongodb_query(validated_query_args) as { [key: string]: any };
93
+ for(let layer of access_layers.layers){
94
+ find[`${layer}_id`] = req.params[layer];
95
+ }
96
+
97
+ let permissive_security_model = await F_Security_Model.model_with_permission(access_layers.security_models, req, res, find, 'get');
98
+ if (!permissive_security_model) {
99
+ res.status(403);
100
+ res.json({ error: `You do not have permission to fetch documents from ${req.params.document_type}.` });
101
+ return;
102
+ }
103
+
104
+ let documents;
105
+ try {
106
+ //@ts-expect-error
107
+ let query = collection.mongoose_model.find(find, undefined, { 'lean': true });
108
+ let fetch = query_object_to_mongodb_limits(query, collection.query_validator_server);
109
+ documents = await fetch;
110
+ } catch(err){
111
+ if (err.name == 'CastError') {
112
+ res.status(400);
113
+ res.json({ error: 'one of the IDs you passed to the query was not a valid MongoDB object ID.' });
114
+ return;
115
+ } else {
116
+ res.status(500);
117
+ res.send({error: 'there was a novel error'});
118
+ console.error(err);
119
+ return;
120
+ }
121
+ }
122
+
123
+ if (!documents) {
124
+ let sendable = await permissive_security_model.handle_empty_query_results(req, res, 'get');
125
+ res.json(sendable);
126
+ } else {
127
+ //await req.schema.handle_pre_send(req, document);
128
+ res.json({ data: documents });
129
+ }
130
+ //await req.schema.fire_api_event('get', req, [document]);
131
+ })
132
+
133
+ let put_path = [
134
+ api_prefix,
135
+ ...base_layers_path_components,
136
+ `${collection.collection_id}/:document_id`
137
+ ].join('/')
138
+
139
+ app.put(put_path, async (req, res) => {
140
+ if (!isValidObjectId(req.params.document_id)) {
141
+ res.status(400);
142
+ res.json({ error: `${req.params.document_id} is not a valid document ID.` });
143
+ return;
144
+ }
145
+
146
+ let find = { '_id': req.params.document_id } as { [key: string]: any } ;
147
+ for(let layer of access_layers.layers){
148
+ find[`${layer}_id`] = req.params[layer];
149
+ }
150
+
151
+
152
+ let permissive_security_model = await F_Security_Model.model_with_permission(access_layers.security_models, req, res, find, 'update');
153
+ if (!permissive_security_model) {
154
+ res.status(403);
155
+ res.json({ error: `You do not have permission to fetch documents from ${req.params.document_type}.` });
156
+ return;
157
+ }
158
+
159
+ if(collection.mongoose_schema.updated_by?.type === String) {
160
+ // if the security schema required the user to be logged in, then req.auth.user_id will not be null
161
+ if((req as Authenticated_Request).auth?.user_id){
162
+ req.body.updated_by = (req as Authenticated_Request).auth?.user_id;
163
+ } else {
164
+ req.body.updated_by = null;
165
+ }
166
+ }
167
+
168
+ if(collection.mongoose_schema.updated_at?.type === Date) {
169
+ req.body.updated_at = new Date();
170
+ }
171
+
172
+ // TODO: it might be possible to build a validator that matches mongoDB's update
173
+ // syntax to allow for targeted updating of nested stuff
174
+ let validated_request_body;
175
+ try {
176
+ validated_request_body = await collection.put_validator.parse(req.body);
177
+ } catch(err){
178
+ if(err instanceof z.ZodError){
179
+ res.status(400);
180
+ res.json({ error: err.issues });
181
+ return;
182
+ } else {
183
+ console.error(err);
184
+ res.status(500);
185
+ res.json({ error: `there was a novel error` });
186
+ return;
187
+ }
188
+ }
189
+
190
+ // if you're accessing the document from /x/:x/y/:y, then you can't change x or y. Note that this does mean if you can access
191
+ // the document from /x/:x, then you'd be able to change y.
192
+ for(let layer of access_layers.layers){
193
+ //@ts-expect-error
194
+ if(validated_request_body[`${layer}_id`] && validated_request_body[`${layer}_id`] !== req.params[layer]){
195
+ res.status(403);
196
+ res.json({ error: `The system does not support changing the ${layer}_id of the document with this endpoint.` });
197
+ return;
198
+ }
199
+ }
200
+
201
+ /*let { error: pre_save_error } = await req.schema.handle_pre_save(req, value);
202
+ if (pre_save_error) {
203
+ res.status(400);
204
+ res.json({ error: pre_save_error.message });
205
+ return;
206
+ }*/
207
+
208
+
209
+ let results;
210
+ try {
211
+ //@ts-expect-error
212
+ results = await collection.mongoose_model.findOneAndUpdate(find, validated_request_body, { returnDocument: 'after', lean: true });
213
+ } catch(err){
214
+ res.status(500);
215
+ res.json({ error: `there was a novel error` });
216
+ console.error(err);
217
+ return;
218
+ }
219
+
220
+ if (!results) {
221
+ let sendable = await permissive_security_model.handle_empty_query_results(req, res, 'update');
222
+ res.json(sendable);
223
+ } else {
224
+ res.json({ data: results });
225
+ }
226
+ //await req.schema.fire_api_event('update', req, results);
227
+ });
228
+
229
+ let post_path = [
230
+ api_prefix,
231
+ ...base_layers_path_components,
232
+ `${collection.collection_id}`
233
+ ].join('/')
234
+
235
+ app.post(post_path, async (req, res) => {
236
+ // I'd like to have a validator here. I think it might need to be a map or record validator?
237
+ let permissive_security_model = await F_Security_Model.model_with_permission(access_layers.security_models, req, res, undefined, 'create');
238
+ if (!permissive_security_model) {
239
+ res.status(403);
240
+ res.json({ error: `You do not have permission to fetch documents from ${req.params.document_type}.` });
241
+ return;
242
+ }
243
+
244
+ if(collection.mongoose_schema.updated_by?.type === String) {
245
+ // if the security schema required the user to be logged in, then req.auth.user_id will not be null
246
+ if((req as Authenticated_Request).auth?.user_id){
247
+ req.body.updated_by = (req as Authenticated_Request).auth?.user_id;
248
+ } else {
249
+ req.body.updated_by = null;
250
+ }
251
+ }
252
+
253
+ if(collection.mongoose_schema.updated_at?.type === Date) {
254
+ req.body.updated_at = new Date();
255
+ }
256
+
257
+ if(collection.mongoose_schema.created_by?.type === String) {
258
+ // if the security schema required the user to be logged in, then req.auth.user_id will not be null
259
+ if((req as Authenticated_Request).auth?.user_id){
260
+ req.body.created_by = (req as Authenticated_Request).auth?.user_id;
261
+ } else {
262
+ req.body.created_by = null;
263
+ }
264
+ }
265
+
266
+ if(collection.mongoose_schema.created_at?.type === Date) {
267
+ req.body.created_at = new Date();
268
+ }
269
+
270
+ let validated_request_body;
271
+ try {
272
+ validated_request_body = await collection.post_validator.parse(req.body);
273
+ } catch(err){
274
+ if(err instanceof z.ZodError){
275
+ res.status(400);
276
+ res.json({ error: err.issues });
277
+ return;
278
+ } else {
279
+ console.error(err);
280
+ res.status(500);
281
+ res.json({ error: `there was a novel error` });
282
+ return;
283
+ }
284
+ }
285
+
286
+ // if you're accessing the document from /x/:x/y/:y, then you can't change x or y. Note that this does mean if you can access
287
+ // the document from /x/:x, then you'd be able to change y.
288
+ for(let layer of access_layers.layers){
289
+ //@ts-expect-error
290
+ if(validated_request_body[`${layer}_id`] && validated_request_body[`${layer}_id`] !== req.params[layer]){
291
+ res.status(403);
292
+ res.json({ error: `The system does not support changing the ${layer}_id of the document with this endpoint.` });
293
+ return;
294
+ }
295
+ }
296
+
297
+ /*let { error: pre_save_error } = await req.schema.handle_pre_save(req, validated_request_body);
298
+ if (pre_save_error) {
299
+ res.status(400);
300
+ res.json({ error: pre_save_error.message });
301
+ return;
302
+ }*/
303
+
304
+ let results;
305
+ try {
306
+ results = await collection.mongoose_model.create(validated_request_body);
307
+ } catch(err){
308
+ res.status(500);
309
+ res.json({ error: `there was a novel error` });
310
+ console.error(err);
311
+ return;
312
+ }
313
+
314
+ if (!results) {
315
+ let sendable = await permissive_security_model.handle_empty_query_results(req, res, 'create');
316
+ res.json(sendable);
317
+ } else {
318
+ res.json({ data: results });
319
+ }
320
+ //await req.schema.fire_api_event('create', req, results);
321
+ });
322
+
323
+ let delete_path = [
324
+ api_prefix,
325
+ ...base_layers_path_components,
326
+ `${collection.collection_id}/:document_id`
327
+ ].join('/')
328
+
329
+ app.delete(delete_path, async (req, res) => {
330
+ if (!isValidObjectId(req.params.document_id)) {
331
+ res.status(400);
332
+ res.json({ error: `${req.params.document_id} is not a valid document ID.` });
333
+ return;
334
+ }
335
+
336
+ let find = { '_id': req.params.document_id } as { [key: string]: any } ;
337
+ for(let layer of access_layers.layers){
338
+ find[`${layer}_id`] = req.params[layer];
339
+ }
340
+
341
+ let permissive_security_model = await F_Security_Model.model_with_permission(access_layers.security_models, req, res, find, 'delete');
342
+ if (!permissive_security_model) {
343
+ res.status(403);
344
+ res.json({ error: `You do not have permission to fetch documents from ${req.params.document_type}.` });
345
+ return;
346
+ }
347
+
348
+ let results;
349
+ try {
350
+ //@ts-expect-error
351
+ results = await collection.mongoose_model.findOneAndDelete(find, {lean: true });
352
+ } catch(err){
353
+ res.status(500);
354
+ res.json({ error: `there was a novel error` });
355
+ console.error(err);
356
+ return;
357
+ }
358
+
359
+ if (!results) {
360
+ let sendable = await permissive_security_model.handle_empty_query_results(req, res, 'delete');
361
+ res.json(sendable);
362
+ } else {
363
+ res.json({ data: results });
364
+ }
365
+ // await req.schema.fire_api_event('delete', req, results);
366
+ });
367
+ }
368
+ }
@@ -0,0 +1,21 @@
1
+ import * as z from "zod/v4";
2
+ import { Request, Response } from "express";
3
+ import { Empty_Query_Possibilities, F_Security_Model, Operation } from "./F_Security_Model.js";
4
+ import { F_Collection } from "../F_Collection.js";
5
+
6
+ export class F_SM_Open_Access<Collection_ID extends string, ZodSchema extends z.ZodObject> extends F_Security_Model<Collection_ID, ZodSchema> {
7
+
8
+ constructor(collection: F_Collection<Collection_ID, ZodSchema>){
9
+ super(collection);
10
+ this.needs_auth_user = false;
11
+ }
12
+
13
+ async has_permission(req: Request, res: Response, find: {[key: string]: any}, operation: Operation): Promise<boolean> {
14
+ return true;
15
+ }
16
+
17
+ async handle_empty_query_results(req: Request, res: Response, operation: Operation): Promise<Empty_Query_Possibilities> {
18
+ return { data: null }
19
+ }
20
+
21
+ }
@@ -0,0 +1,72 @@
1
+ import * as z from "zod/v4";
2
+ import { Request, Response } from "express";
3
+ import { F_Collection } from "../F_Collection.js";
4
+ import { Authenticated_Request, Empty_Query_Possibilities, F_Security_Model, Operation } from "./F_Security_Model.js";
5
+
6
+ export class F_SM_Ownership<Collection_ID extends string, ZodSchema extends z.ZodObject> extends F_Security_Model<Collection_ID, ZodSchema> {
7
+ user_id_field: string;
8
+
9
+ constructor(collection: F_Collection<Collection_ID, ZodSchema>, user_id_field = 'user_id'){
10
+ super(collection);
11
+ this.needs_auth_user = true;
12
+ this.user_id_field = user_id_field;
13
+ }
14
+
15
+ async has_permission(req: Authenticated_Request, res: Response, find: {[key: string]: any}, operation: Operation): Promise<boolean> {
16
+ let user_id = '' + req.auth.user_id;
17
+
18
+ if (operation === 'get') {
19
+ // if we're fetching a specific document by its ID, it's valid to get
20
+ // it as long as we modify the find so that it only returns documents
21
+ // owned by the current user
22
+ if (req.params.document_id) {
23
+ find[this.user_id_field] = user_id;
24
+
25
+ return true;
26
+ }
27
+
28
+ // if we're fetching a document and filtering by the user's ID already,
29
+ // then this security model is satisfied
30
+ if (find[this.user_id_field] === user_id) {
31
+ return true;
32
+ }
33
+ }
34
+
35
+ // if we're updating a specific document, it's valid as long as we modify the
36
+ // find so that it only modifies documents owned by the current user
37
+ if (operation === 'update') {
38
+ find[this.user_id_field] = user_id;
39
+ return true;
40
+ }
41
+
42
+ // if we're creating a document, it's valid as long as it's owned by the current
43
+ // user.
44
+ if (operation === 'create') {
45
+ if (req.body[this.user_id_field] === user_id) {
46
+ return true;
47
+ }
48
+ }
49
+
50
+ // if we're deleting a specific document, it's valid as long as we modify the
51
+ // find so that it only deletes documents owned by the current user
52
+ if (operation === 'delete') {
53
+ find[this.user_id_field] = user_id;
54
+ return true;
55
+ }
56
+
57
+ return false;
58
+ }
59
+
60
+ async handle_empty_query_results(req: Request, res: Response, operation: Operation): Promise<Empty_Query_Possibilities> {
61
+ if (req.params.document_id) {
62
+ let document_result = await this.collection.mongoose_model.findById(req.params.document_id);
63
+
64
+ if (document_result) {
65
+ res.status(403);
66
+ return { error: `You do not have permission to ${operation} documents from ${req.params.document_type}.` };
67
+ }
68
+ }
69
+
70
+ return { data: null };
71
+ }
72
+ }
@@ -0,0 +1,87 @@
1
+ import * as z from "zod/v4";
2
+ import { Request, Response } from "express";
3
+ import { F_Collection } from "../F_Collection.js";
4
+ import { Cache } from "../utils/cache.js";
5
+ import { Authenticated_Request, Empty_Query_Possibilities, F_Security_Model, Operation } from "./F_Security_Model.js";
6
+ import mongoose from "mongoose";
7
+
8
+ let operation_permission_map = {
9
+ 'get': 'read',
10
+ 'create': 'create',
11
+ 'update': 'update',
12
+ 'delete': 'delete'
13
+ }
14
+
15
+ export class F_SM_Role_Membership<Collection_ID extends string, ZodSchema extends z.ZodObject> extends F_Security_Model<Collection_ID, ZodSchema> {
16
+ user_id_field: string;
17
+ role_id_field: string;
18
+ layer_collection_id: string;
19
+ role_membership_collection: F_Collection<string, any>;
20
+ role_membership_cache: Cache<any>;
21
+ role_collection: F_Collection<string, any>;
22
+ role_cache: Cache<any>;
23
+
24
+ constructor(collection: F_Collection<Collection_ID, ZodSchema>,
25
+ layer_collection: F_Collection<string, any>,
26
+ role_membership_collection: F_Collection<string, any>,
27
+ role_collection: F_Collection<string, any>,
28
+ role_membership_cache?: Cache<any>,
29
+ role_cache?: Cache<any>,
30
+ user_id_field = 'user_id',
31
+ role_id_field = 'role_id',
32
+ ){
33
+ super(collection);
34
+ this.needs_auth_user = true;
35
+ this.user_id_field = user_id_field;
36
+ this.role_id_field = role_id_field;
37
+ this.layer_collection_id = layer_collection.collection_id;
38
+ this.role_membership_collection = role_membership_collection;
39
+ this.role_membership_cache = role_membership_cache ?? new Cache(60);
40
+ this.role_collection = role_collection;
41
+ this.role_cache = role_cache ?? new Cache(60);
42
+
43
+ if(!this.role_collection.mongoose_schema.permissions){
44
+ throw new Error(`could not find field "permissions" on role collection. Permissions should be an object of the format {[key: collection_id]: ('read'| 'create'| 'update'| 'delete')[]}`)
45
+ }
46
+ }
47
+
48
+ async has_permission(req: Authenticated_Request, res: Response, find: {[key: string]: any}, operation: Operation): Promise<boolean> {
49
+ let user_id = req.auth.user_id;
50
+ let layer_id = req.params[this.layer_collection_id];
51
+
52
+ // return the role membership associated with the layer. This uses the cache heavily, so it should be
53
+ // a cheap operation even though it makes an extra database query. Use the cache's first_fetch_then_refresh
54
+ // method so that we aren't keeping out-of-date auth data in the cache.
55
+ let role_membership = await this.role_membership_cache.first_fetch_then_refresh(`${user_id}-${layer_id}`, async () => {
56
+ let role_memberships = await this.role_membership_collection.mongoose_model.find({
57
+ [this.user_id_field]: user_id,
58
+ [`${this.layer_collection_id}_id`]: new mongoose.Types.ObjectId(layer_id)
59
+ })
60
+
61
+ if(role_memberships.length > 1){
62
+ console.warn(`in F_SM_Role_Membership, more than one role membership for user ${user_id} at layer ${this.layer_collection_id} found.`)
63
+ }
64
+ return role_memberships[0];
65
+ })
66
+
67
+ if(!role_membership){ return false; }
68
+ if(!role_membership[this.role_id_field]){ console.warn(`role membership collection ${this.role_membership_collection.collection_id} did not have role ID filed ${this.role_id_field}`); return false;}
69
+
70
+ // return the role associated with the role membership. This uses the cache heavily, so it should be
71
+ // a cheap operation even though it makes an extra database query. Use the cache's first_fetch_then_refresh
72
+ // method so that we aren't keeping out-of-date auth data in the cache.
73
+ let role = await this.role_cache.first_fetch_then_refresh(role_membership[this.role_id_field], async () => {
74
+ let role = await this.role_collection.mongoose_model.findById(role_membership[this.role_id_field]);
75
+ return role;
76
+ })
77
+
78
+ if(!role){ return false; }
79
+ if(!role.permissions){ console.warn(`role collection ${this.role_collection.collection_id} was missing its permissions field`); return false; }
80
+ if(!role.permissions[this.collection.collection_id]){ console.warn(`role collection ${this.role_collection.collection_id} was missing its permissions.${this.collection.collection_id} field`); return false; }
81
+ return role.permissions[this.collection.collection_id].includes(operation_permission_map[operation]);
82
+ }
83
+
84
+ async handle_empty_query_results(req: Request, res: Response, operation: Operation): Promise<Empty_Query_Possibilities> {
85
+ return { data: null };
86
+ }
87
+ }
@@ -0,0 +1,85 @@
1
+
2
+ import * as z from "zod/v4";
3
+ import { Router, Request, Response } from "express";
4
+ import { F_Collection } from "../F_Collection.js";
5
+
6
+ export type Operation = 'get' | 'update' | 'create' | 'delete';
7
+
8
+ export abstract class F_Security_Model<Collection_ID extends string, ZodSchema extends z.ZodObject> {
9
+ collection: F_Collection<Collection_ID, ZodSchema>;
10
+ needs_auth_user: boolean
11
+ static auth_fetcher: (req: Request) => Promise<Auth_Data | undefined>
12
+
13
+ constructor(collection: F_Collection<Collection_ID, ZodSchema>) {
14
+ this.collection = collection;
15
+ }
16
+
17
+ /**
18
+ * Returns true if this security model can grant permission to perform the operation.
19
+ * This may be accomplished by modifying the mongodb filter that will be applied during
20
+ * the fetch/update: for example, a security model that allows modifying only members of
21
+ * a certain institution might add { institution_id: xxxxx } to the find.
22
+ * @param req
23
+ * @param res
24
+ * @param find
25
+ * @param operation
26
+ */
27
+ abstract has_permission(req: Request, res: Response, find: {[key: string]: any}, operation: Operation): Promise<boolean>;
28
+
29
+ /**
30
+ * In the event that no documents are returned by the mongodb operation, it's necessary to find
31
+ * out if this was caused by modifications to the mongodb filter, or if it was caused by a lack
32
+ * of documents to operate on.
33
+ *
34
+ * In the event that there are documents but the user lacks permission to act on them, this method
35
+ * is expected to set the response status to 403.
36
+ *
37
+ * @param req
38
+ * @param res
39
+ * @param operation
40
+ */
41
+ abstract handle_empty_query_results(req: Request, res: Response, operation: Operation): Promise<Empty_Query_Possibilities>;
42
+
43
+ static set_auth_fetcher(fetcher: (req: Request) => Promise<Auth_Data | undefined>){
44
+ F_Security_Model.auth_fetcher = fetcher;
45
+ }
46
+
47
+ static async has_permission(models: F_Security_Model<string, any>[], req: Request, res: Response, find: {[key: string]: any}, operation: Operation){ console.error(`F_Security_Model.has_permission is deprecated in favor of F_Security_Model.model_with_permission.`); return await F_Security_Model.model_with_permission(models, req, res, find, operation); }
48
+ static async model_with_permission(models: F_Security_Model<string, any>[], req: Request, res: Response, find: {[key: string]: any}, operation: Operation): Promise<F_Security_Model<string, any>> {
49
+ let has_attempted_authenticating_user = false;
50
+
51
+ for (let security_model of models) {
52
+ // if the security model needs user auth data, fetch it.
53
+ if (security_model.needs_auth_user && !has_attempted_authenticating_user) {
54
+ has_attempted_authenticating_user = true;
55
+ (req as Authenticated_Request).auth = await F_Security_Model.auth_fetcher(req);
56
+ }
57
+
58
+ // if the user auth data fetch failed, and the security model needs that data, don't bother using the security model.
59
+ if (security_model.needs_auth_user && !(req as Authenticated_Request).auth) {
60
+ continue;
61
+ }
62
+
63
+ // if the security model is a good candidate, return it.
64
+ if (await security_model.has_permission(req, res, find, operation)) {
65
+ return security_model;
66
+ }
67
+ }
68
+ return undefined;
69
+ }
70
+ }
71
+
72
+ export type Auth_Data = {
73
+ user_id: string,
74
+ layers: {
75
+ layer_id: string,
76
+ permissions: {[key: string]: Operation[]},
77
+ special_permissions: {[key: string]: string[]}
78
+ }[]
79
+ }
80
+
81
+ export type Authenticated_Request = Request & {
82
+ auth: Auth_Data
83
+ }
84
+
85
+ export type Empty_Query_Possibilities = { data: null} | { error: string };