@rapidd/build 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rapidd/build",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Dynamic code generator that transforms Prisma schemas into Express.js CRUD APIs with PostgreSQL RLS-to-JavaScript translation",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -12,7 +12,8 @@ const { generateAllRoutes } = require('../generators/routeGenerator');
12
12
  */
13
13
  function generateBaseModelFile(modelJsPath) {
14
14
  const content = `const { QueryBuilder, prisma } = require("./QueryBuilder");
15
- const {ErrorResponse, getTranslation} = require('./Api');
15
+ const {rls} = require('../../rapidd/rapidd');
16
+ const {ErrorResponse} = require('./Api');
16
17
 
17
18
  class Model {
18
19
  /**
@@ -21,17 +22,23 @@ class Model {
21
22
  */
22
23
  constructor(name, options){
23
24
  this.modelName = name;
25
+ this.queryBuilder = new QueryBuilder(name);
26
+ this.rls = rls.model[name] || {};
24
27
  this.options = options || {}
25
28
  this.user = this.options.user || {'id': 1, 'role': 'application'};
26
29
  this.user_id = this.user ? this.user.id : null;
27
30
  }
28
31
 
29
- _select = (fields) => this.constructor.queryBuilder.select(fields);
30
- _filter = (q) => this.constructor.queryBuilder.filter(q);
31
- _include = (include) => this.constructor.queryBuilder.include(include, this.user);
32
- _getAccessFilter = () => this.constructor.getAccessFilter(this.user);
33
- _hasAccess = (data) => this.constructor.hasAccess(data, this.user) || false;
34
- _omit = () => this.constructor.queryBuilder.omit(this.user);
32
+ _select = (fields) => this.queryBuilder.select(fields);
33
+ _filter = (q) => this.queryBuilder.filter(q);
34
+ _include = (include) => this.queryBuilder.include(include, this.user);
35
+ // RLS METHODS
36
+ _canCreate = () => this.rls.canCreate?.(this.user) || false;
37
+ _hasAccess = (data) => this.rls.hasAccess?.(data, this.user) || false;
38
+ _getAccessFilter = () => this.rls.getAccessFilter?.(this.user);
39
+ _getUpdateFilter = () => this.rls.getUpdateFilter?.(this.user);
40
+ _getDeleteFilter = () => this.rls.getDeleteFilter?.(this.user);
41
+ _omit = () => this.queryBuilder.omit(this.user);
35
42
 
36
43
  /**
37
44
  *
@@ -50,8 +57,7 @@ class Model {
50
57
  sortBy = sortBy.trim();
51
58
  sortOrder = sortOrder.trim();
52
59
  if (!sortBy.includes('.') && this.fields[sortBy] == undefined) {
53
- const message = getTranslation("invalid_sort_field", {sortBy, modelName: this.constructor.name});
54
- throw new ErrorResponse(message, 400);
60
+ throw new ErrorResponse(400, "invalid_sort_field", {sortBy, modelName: this.constructor.name});
55
61
  }
56
62
 
57
63
  // Query the database using Prisma with filters, pagination, and limits
@@ -114,8 +120,13 @@ class Model {
114
120
  * @returns {Promise<Object>}
115
121
  */
116
122
  _create = async (data, options = {}) => {
123
+ // CHECK CREATE PERMISSION
124
+ if (!this.canCreate()) {
125
+ throw new ErrorResponse(403, "no_permission_to_create");
126
+ }
127
+
117
128
  // VALIDATE PASSED FIELDS AND RELATIONSHIPS
118
- this.constructor.queryBuilder.create(data, this.user_id);
129
+ this.queryBuilder.create(data, this.user_id);
119
130
 
120
131
  // CREATE
121
132
  return await this.prisma.create({
@@ -132,11 +143,32 @@ class Model {
132
143
  */
133
144
  _update = async (id, data, options = {}) => {
134
145
  id = Number(id);
135
- // GET DATA FIRST
146
+
147
+ // CHECK UPDATE PERMISSION
148
+ const updateFilter = this.getUpdateFilter();
149
+ if (updateFilter === false) {
150
+ throw new ErrorResponse(403, "no_permission_to_update");
151
+ }
152
+
153
+ // GET DATA FIRST (also checks read access)
136
154
  const current_data = await this._get(id, "ALL");
137
155
 
156
+ // If updateFilter is not empty, verify the record matches the filter
157
+ if (Object.keys(updateFilter).length > 0) {
158
+ const canUpdate = await this.prisma.findUnique({
159
+ 'where': {
160
+ 'id': id,
161
+ ...updateFilter
162
+ },
163
+ 'select': { 'id': true }
164
+ });
165
+ if (!canUpdate) {
166
+ throw new ErrorResponse(403, "no_permission_to_update");
167
+ }
168
+ }
169
+
138
170
  // VALIDATE PASSED FIELDS AND RELATIONSHIPS
139
- this.constructor.queryBuilder.update(id, data, this.user_id);
171
+ this.queryBuilder.update(id, data, this.user_id);
140
172
  return await this.prisma.update({
141
173
  'where': {
142
174
  'id': id
@@ -163,9 +195,31 @@ class Model {
163
195
  * @returns {Promise<Object>}
164
196
  */
165
197
  _delete = async (id, options = {}) => {
166
- // GET DATA FIRST
198
+ id = Number(id);
199
+
200
+ // CHECK DELETE PERMISSION
201
+ const deleteFilter = this.getDeleteFilter();
202
+ if (deleteFilter === false) {
203
+ throw new ErrorResponse(403, "no_permission_to_delete");
204
+ }
205
+
206
+ // GET DATA FIRST (also checks read access)
167
207
  const current_data = await this._get(id);
168
208
 
209
+ // If deleteFilter is not empty, verify the record matches the filter
210
+ if (Object.keys(deleteFilter).length > 0) {
211
+ const canDelete = await this.prisma.findUnique({
212
+ 'where': {
213
+ 'id': id,
214
+ ...deleteFilter
215
+ },
216
+ 'select': { 'id': true }
217
+ });
218
+ if (!canDelete) {
219
+ throw new ErrorResponse(403, "no_permission_to_delete");
220
+ }
221
+ }
222
+
169
223
  return await this.prisma.delete({
170
224
  'where': {
171
225
  id: parseInt(id)
@@ -233,10 +287,10 @@ class Model {
233
287
  return this._include(include);
234
288
  }
235
289
  sort(sortBy, sortOrder) {
236
- return this.constructor.queryBuilder.sort(sortBy, sortOrder);
290
+ return this.queryBuilder.sort(sortBy, sortOrder);
237
291
  }
238
292
  take(limit){
239
- return this.constructor.queryBuilder.take(Number(limit));
293
+ return this.queryBuilder.take(Number(limit));
240
294
  }
241
295
  skip(offset){
242
296
  const parsed = parseInt(offset);
@@ -267,6 +321,39 @@ class Model {
267
321
  return this.user.role == "application" ? true : this._hasAccess(data, this.user);
268
322
  }
269
323
 
324
+ /**
325
+ * Check if user can create records
326
+ * @returns {boolean}
327
+ */
328
+ canCreate() {
329
+ if(this.user.role == "application") return true;
330
+ return this._canCreate();
331
+ }
332
+
333
+ /**
334
+ * Get update filter for RLS
335
+ * @returns {Object|false}
336
+ */
337
+ getUpdateFilter(){
338
+ const filter = this._getUpdateFilter();
339
+ if(this.user.role == "application" || filter == true){
340
+ return {};
341
+ }
342
+ return filter;
343
+ }
344
+
345
+ /**
346
+ * Get delete filter for RLS
347
+ * @returns {Object|false}
348
+ */
349
+ getDeleteFilter(){
350
+ const filter = this._getDeleteFilter();
351
+ if(this.user.role == "application" || filter == true){
352
+ return {};
353
+ }
354
+ return filter;
355
+ }
356
+
270
357
  set modelName (name){
271
358
  this.name = name;
272
359
  this.prisma = prisma[name];
@@ -12,21 +12,10 @@ function generateModelFile(modelName, modelInfo) {
12
12
  const className = modelName.charAt(0).toUpperCase() + modelName.slice(1);
13
13
 
14
14
  return `const {Model, QueryBuilder, prisma} = require('../Model');
15
- const {rls} = require('../../rapidd/rapidd');
16
15
 
17
16
  class ${className} extends Model {
18
17
  constructor(options){
19
- super('${modelName}', options);
20
- }
21
-
22
- static queryBuilder = new QueryBuilder('${modelName}', rls.model.${modelName} || {});
23
-
24
- static getAccessFilter(user) {
25
- return rls.model.${modelName}?.getAccessFilter?.(user) || {};
26
- }
27
-
28
- static hasAccess(data, user) {
29
- return rls.model.${modelName}?.hasAccess?.(data, user) || true;
18
+ super('${className}', options);
30
19
  }
31
20
 
32
21
  /**