zrb 1.2.1__py3-none-any.whl → 1.3.0__py3-none-any.whl

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 (41) hide show
  1. zrb/builtin/llm/llm_chat.py +68 -9
  2. zrb/builtin/llm/tool/api.py +4 -2
  3. zrb/builtin/llm/tool/file.py +39 -0
  4. zrb/builtin/llm/tool/rag.py +37 -22
  5. zrb/builtin/llm/tool/web.py +46 -20
  6. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/column/add_column_util.py +28 -6
  7. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/module/gateway/view/content/my-module/my-entity.html +206 -178
  8. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/schema/my_entity.py +3 -1
  9. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/repository/role_db_repository.py +18 -1
  10. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/repository/role_repository.py +4 -0
  11. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/role_service.py +20 -11
  12. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_db_repository.py +17 -2
  13. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_repository.py +4 -0
  14. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service.py +19 -11
  15. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/permission.html +209 -180
  16. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/role.html +362 -0
  17. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/user.html +377 -0
  18. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/common/util.js +68 -13
  19. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/crud/util.js +50 -29
  20. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/permission.py +3 -1
  21. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/role.py +6 -5
  22. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/user.py +9 -3
  23. zrb/config.py +3 -1
  24. zrb/content_transformer/content_transformer.py +7 -1
  25. zrb/context/context.py +8 -2
  26. zrb/input/any_input.py +5 -0
  27. zrb/input/base_input.py +6 -0
  28. zrb/input/bool_input.py +2 -0
  29. zrb/input/float_input.py +2 -0
  30. zrb/input/int_input.py +2 -0
  31. zrb/input/option_input.py +2 -0
  32. zrb/input/password_input.py +2 -0
  33. zrb/input/text_input.py +11 -5
  34. zrb/runner/cli.py +1 -1
  35. zrb/runner/common_util.py +3 -3
  36. zrb/runner/web_route/task_input_api_route.py +1 -1
  37. zrb/task/llm_task.py +103 -16
  38. {zrb-1.2.1.dist-info → zrb-1.3.0.dist-info}/METADATA +85 -18
  39. {zrb-1.2.1.dist-info → zrb-1.3.0.dist-info}/RECORD +41 -40
  40. {zrb-1.2.1.dist-info → zrb-1.3.0.dist-info}/WHEEL +0 -0
  41. {zrb-1.2.1.dist-info → zrb-1.3.0.dist-info}/entry_points.txt +0 -0
@@ -1,23 +1,31 @@
1
1
  <link rel="stylesheet" href="/static/crud/style.css">
2
2
 
3
- <main class="container">
3
+ <main id="crud-app"
4
+ class="container"
5
+ data-page-size='{{ page_size | tojson }}'
6
+ data-page='{{ page | tojson }}'
7
+ data-sort='{{ sort | tojson }}'
8
+ data-filter='{{ filter | tojson }}'
9
+ data-allow-create='{{ allow_create | tojson }}'
10
+ data-allow-update='{{ allow_update | tojson }}'
11
+ data-allow-delete='{{ allow_delete | tojson }}'>
4
12
  <article>
5
13
  <h1>My Entity</h1>
6
14
 
7
15
  <fieldset id="crud-table-fieldset" role="group" class="grid">
8
- <input id="crud-filter" onchange="applySearch()" placeholder="🔍 Filter" aria-label="Search" />
9
- <button onclick="applySearch()">🔍 Search</button>
16
+ <input id="crud-filter-input" placeholder="🔍 Filter" aria-label="Search" />
17
+ <button id="crud-search-button">🔍 Search</button>
10
18
  {% if allow_create %}
11
- <button class="contrast" onclick="showCreateForm(event)">➕ Add</button>
19
+ <button id="crud-show-create-button" class="contrast">➕ Add</button>
12
20
  {% endif %}
13
21
  </fieldset>
22
+
14
23
  <div id="crud-table-container">
15
24
  <table id="crud-table" class="striped">
16
25
  <thead>
17
26
  <tr>
18
27
  <th scope="col">ID</th>
19
28
  <th scope="col">My Column</th>
20
- <!-- Update this -->
21
29
  {% if allow_update or allow_delete %}
22
30
  <th scope="col">Actions</th>
23
31
  {% endif %}
@@ -37,10 +45,9 @@
37
45
  My Column:
38
46
  <input type="text" name="my_column" required>
39
47
  </label>
40
- <!-- Update this -->
41
48
  <footer>
42
- <button onclick="createRow(event)">➕ Save</button>
43
- <button class="secondary" onclick="hideCreateForm(event)">❌ Cancel</button>
49
+ <button id="crud-create-button">➕ Save</button>
50
+ <button id="crud-cancel-create-button" class="secondary">❌ Cancel</button>
44
51
  </footer>
45
52
  </form>
46
53
  </article>
@@ -56,10 +63,9 @@
56
63
  My Column:
57
64
  <input type="text" name="my_column" required>
58
65
  </label>
59
- <!-- Update this -->
60
66
  <footer>
61
- <button onclick="updateRow(event)">✏️ Save</button>
62
- <button class="secondary" onclick="hideUpdateForm(event)">❌ Cancel</button>
67
+ <button id="crud-update-button">✏️ Save</button>
68
+ <button id="crud-cancel-update-button" class="secondary">❌ Cancel</button>
63
69
  </footer>
64
70
  </form>
65
71
  </article>
@@ -75,10 +81,9 @@
75
81
  My Column:
76
82
  <input type="text" name="my_column" readonly>
77
83
  </label>
78
- <!-- Update this -->
79
84
  <footer>
80
- <button class="secondary" onclick="hideDeleteForm()">❌ Cancel</button>
81
- <button onclick="deleteRow()">🗑️ Delete</button>
85
+ <button id="crud-cancel-delete-button" class="secondary">❌ Cancel</button>
86
+ <button id="crud-delete-button">🗑️ Delete</button>
82
87
  </footer>
83
88
  </form>
84
89
  </article>
@@ -90,208 +95,231 @@
90
95
  <h2 id="crud-alert-title">Error</h2>
91
96
  <pre id="crud-alert-message"></pre>
92
97
  <footer>
93
- <button onclick="hideAlert(event)">Close</button>
98
+ <button id="crud-alert-close-button">Close</button>
94
99
  </footer>
95
100
  </article>
96
101
  </dialog>
97
-
98
102
  </article>
99
103
  </main>
100
104
 
101
105
  <script src="/static/crud/util.js"></script>
102
106
  <script>
103
- const apiUrl = "/api/v1/my-entities";
104
- const crudState = {
105
- pageSize: {{page_size | tojson}},
106
- currentPage: {{page | tojson}},
107
- sort: {{sort | tojson}},
108
- filter: {{filter | tojson}},
109
- allowCreate: {{allow_create | tojson}},
110
- allowUpdate: {{allow_update | tojson}},
111
- allowDelete: {{allow_delete | tojson}},
112
- updatedRowId: null,
113
- deletedRowId: null,
114
- };
107
+ class CrudApp {
108
+ constructor(apiUrl, initialState) {
109
+ this.apiUrl = apiUrl;
110
+ this.state = { ...initialState };
111
+ this.init();
112
+ }
115
113
 
116
- async function applySearch() {
117
- const filterInput = document.getElementById("crud-filter");
118
- crudState.filter = filterInput.value;
119
- return await fetchRows(crudState.currentPage);
120
- }
114
+ init() {
115
+ // Cache common elements
116
+ this.filterInput = document.getElementById("crud-filter-input");
117
+ this.searchButton = document.getElementById("crud-search-button");
118
+ this.filterInput.value = this.state.filter;
121
119
 
122
- async function fetchRows(page = null) {
123
- try {
124
- if (typeof page !== 'undefined' && page !== null) {
125
- crudState.currentPage = page;
126
- }
127
- const defaultSearchColumn = "my_column"
128
- // update address bar
129
- const searchParam = CRUD_UTIL.getSearchParam(crudState, defaultSearchColumn, false);
130
- const newUrl = `${window.location.pathname}?${searchParam}`;
131
- window.history.pushState({ path: newUrl }, "", newUrl);
132
- // update table and pagination
133
- const apiSearchParam = CRUD_UTIL.getSearchParam(crudState, defaultSearchColumn, true);
134
- const result = await UTIL.fetchAPI(
135
- `${apiUrl}?${apiSearchParam}`, { method: "GET" }
136
- );
137
- renderRows(result.data);
138
- const crudPagination = document.getElementById("crud-pagination");
139
- CRUD_UTIL.renderPagination(
140
- crudPagination, crudState, result.count, "fetchRows"
141
- );
142
- } catch (error) {
143
- console.error("Error fetching items:", error);
120
+ this.filterInput.addEventListener("change", (e) => this.applySearch(e));
121
+ this.searchButton.addEventListener("click", (e) => this.applySearch(e));
122
+
123
+ // Attach optional events if elements exist
124
+ this.attachEvent("crud-show-create-button", this.showCreateForm.bind(this));
125
+ this.attachEvent("crud-create-button", this.createRow.bind(this));
126
+ this.attachEvent("crud-cancel-create-button", this.hideCreateForm.bind(this));
127
+ this.attachEvent("crud-update-button", this.updateRow.bind(this));
128
+ this.attachEvent("crud-cancel-update-button", this.hideUpdateForm.bind(this));
129
+ this.attachEvent("crud-delete-button", this.deleteRow.bind(this));
130
+ this.attachEvent("crud-cancel-delete-button", this.hideDeleteForm.bind(this));
131
+ this.attachEvent("crud-alert-close-button", this.hideAlert.bind(this));
132
+
133
+ // Initial data fetch
134
+ this.fetchRows(this.state.currentPage);
144
135
  }
145
- }
146
136
 
147
- function renderRows(rows) {
148
- const tableBody = document.querySelector("#crud-table tbody");
149
- tableBody.innerHTML = "";
150
- rows.forEach(row => {
151
- let rowComponent = getRowComponents(row);
152
- actionColumn = "";
153
- if (crudState.allowUpdate) {
154
- actionColumn += `<button class="contrast" onclick="showUpdateForm('${row.id}')">✏️ Edit</button>`;
155
- }
156
- if (crudState.allowDelete) {
157
- actionColumn += `<button class="secondary" onclick="showDeleteForm('${row.id}')">🗑️ Delete</button>`;
158
- }
159
- if (crudState.allowUpdate || crudState.allowDelete) {
160
- actionColumn = `<td><fieldset class="grid" role="group">${actionColumn}</fieldset></td>`;
137
+ attachEvent(elementId, handler) {
138
+ const el = document.getElementById(elementId);
139
+ if (el) el.addEventListener("click", handler);
140
+ }
141
+
142
+ async applySearch(event) {
143
+ if (event) event.preventDefault();
144
+ this.state.filter = this.filterInput.value;
145
+ await this.fetchRows(this.state.currentPage);
146
+ }
147
+
148
+ async fetchRows(page = null) {
149
+ try {
150
+ if (page !== null) {
151
+ this.state.currentPage = page;
152
+ }
153
+ const defaultSearchColumn = "my_column";
154
+ // Update address bar
155
+ const searchParam = CRUD_UTIL.getSearchParam(this.state, defaultSearchColumn, false);
156
+ const newUrl = `${window.location.pathname}?${searchParam}`;
157
+ window.history.pushState({ path: newUrl }, "", newUrl);
158
+
159
+ // Fetch table data
160
+ const apiSearchParam = CRUD_UTIL.getSearchParam(this.state, defaultSearchColumn, true);
161
+ const result = await UTIL.fetchAPI(`${this.apiUrl}?${apiSearchParam}`, { method: "GET" });
162
+ this.renderRows(result.data);
163
+ const crudPagination = document.getElementById("crud-pagination");
164
+ CRUD_UTIL.renderPagination(crudPagination, this, result.count);
165
+ } catch (error) {
166
+ console.error("Error fetching items:", error);
161
167
  }
162
- tableBody.innerHTML += `<tr>${rowComponent.join('')}${actionColumn}</tr>`;
163
- });
164
- }
168
+ }
165
169
 
166
- function getRowComponents(row) {
167
- let rowComponents = [];
168
- rowComponents.push(`<td>${row.id}</td>`);
169
- rowComponents.push(`<td>${row.my_column}</td>`);
170
- // Update this
171
- return rowComponents;
172
- }
170
+ renderRows(rows) {
171
+ const tableBody = document.querySelector("#crud-table tbody");
172
+ let tableBodyHTML = "";
173
+ rows.forEach(row => {
174
+ const rowComponents = this.getRowComponents(row);
175
+ let actionColumn = "";
176
+ if (this.state.allowUpdate) {
177
+ actionColumn += `<button class="contrast" data-id="${row.id}" data-action="edit">✏️ Edit</button>`;
178
+ }
179
+ if (this.state.allowDelete) {
180
+ actionColumn += `<button class="secondary" data-id="${row.id}" data-action="delete">🗑️ Delete</button>`;
181
+ }
182
+ if (this.state.allowUpdate || this.state.allowDelete) {
183
+ actionColumn = `<td><fieldset class="grid" role="group">${actionColumn}</fieldset></td>`;
184
+ }
185
+ tableBodyHTML += `<tr>${rowComponents.join('')}${actionColumn}</tr>`;
186
+ });
187
+ tableBody.innerHTML = tableBodyHTML;
188
+ this.attachRowActionListeners();
189
+ }
173
190
 
174
- {% if allow_create %}
175
- async function showCreateForm(id) {
176
- const createFormDialog = document.getElementById("crud-create-form-dialog");
177
- const createForm = document.getElementById("crud-create-form");
178
- UTIL.clearFormData(createForm);
179
- createFormDialog.showModal();
180
- }
191
+ attachRowActionListeners() {
192
+ document.querySelectorAll('button[data-action="edit"]').forEach(button => {
193
+ button.addEventListener("click", () => {
194
+ this.showUpdateForm(button.getAttribute("data-id"));
195
+ });
196
+ });
197
+ document.querySelectorAll('button[data-action="delete"]').forEach(button => {
198
+ button.addEventListener("click", () => {
199
+ this.showDeleteForm(button.getAttribute("data-id"));
200
+ });
201
+ });
202
+ }
181
203
 
182
- async function createRow(event = null) {
183
- if (event != null) {
184
- event.preventDefault();
204
+ getRowComponents(row) {
205
+ const rowComponents = [`<td>${row.id}</td>`, `<td>${row.my_column}</td>`];
206
+ return rowComponents;
185
207
  }
186
- try {
208
+
209
+ // Create methods
210
+ showCreateForm(event = null) {
211
+ if (event) event.preventDefault();
212
+ const createDialog = document.getElementById("crud-create-form-dialog");
187
213
  const createForm = document.getElementById("crud-create-form");
188
- const formData = JSON.stringify(UTIL.getFormData(createForm));
189
- await UTIL.fetchAPI(apiUrl, {method: "POST", body: formData});
190
- await fetchRows();
191
- hideCreateForm();
192
- } catch(error) {
193
- showAlert("Create My Entity Error", error);
214
+ UTIL.clearFormData(createForm);
215
+ createDialog.showModal();
194
216
  }
195
- }
196
217
 
197
- function hideCreateForm(event = null) {
198
- if (event != null) {
199
- event.preventDefault();
218
+ async createRow(event = null) {
219
+ if (event) event.preventDefault();
220
+ try {
221
+ const createForm = document.getElementById("crud-create-form");
222
+ const formData = UTIL.getFormData(createForm);
223
+ await UTIL.fetchAPI(this.apiUrl, { method: "POST", body: JSON.stringify(formData) });
224
+ await this.fetchRows();
225
+ this.hideCreateForm();
226
+ } catch (error) {
227
+ console.error(error);
228
+ this.showAlert("Create My Entity Error", error);
229
+ }
200
230
  }
201
- const createFormDialog = document.getElementById("crud-create-form-dialog");
202
- createFormDialog.close();
203
- }
204
- {% endif %}
205
231
 
206
- {% if allow_update %}
207
- async function showUpdateForm(id) {
208
- crudState.updatedRowId = id;
209
- const updateFormDialog = document.getElementById("crud-update-form-dialog");
210
- const updateForm = document.getElementById("crud-update-form");
211
- result = await UTIL.fetchAPI(`${apiUrl}/${id}`, { method: "GET" });
212
- UTIL.setFormData(updateForm, result);
213
- updateFormDialog.showModal();
214
- }
215
-
216
- async function updateRow(event = null) {
217
- if (event != null) {
218
- event.preventDefault();
232
+ hideCreateForm(event = null) {
233
+ if (event) event.preventDefault();
234
+ document.getElementById("crud-create-form-dialog").close();
219
235
  }
220
- try {
236
+
237
+ // Update methods
238
+ async showUpdateForm(id) {
239
+ this.state.updatedRowId = id;
240
+ const updateDialog = document.getElementById("crud-update-form-dialog");
221
241
  const updateForm = document.getElementById("crud-update-form");
222
- const formData = JSON.stringify(UTIL.getFormData(updateForm));
223
- await UTIL.fetchAPI(
224
- `${apiUrl}/${crudState.updatedRowId}`, {method: "PUT", body: formData},
225
- );
226
- await fetchRows();
227
- hideUpdateForm();
228
- } catch(error) {
229
- showAlert("Update My Entity Error", error);
242
+ const formData = await UTIL.fetchAPI(`${this.apiUrl}/${id}`, { method: "GET" });
243
+ UTIL.setFormData(updateForm, formData);
244
+ updateDialog.showModal();
230
245
  }
231
- }
232
246
 
233
- function hideUpdateForm(event = null) {
234
- if (event != null) {
235
- event.preventDefault();
247
+ async updateRow(event = null) {
248
+ if (event) event.preventDefault();
249
+ try {
250
+ const updateForm = document.getElementById("crud-update-form");
251
+ const formData = UTIL.getFormData(updateForm);
252
+ await UTIL.fetchAPI(`${this.apiUrl}/${this.state.updatedRowId}`, {
253
+ method: "PUT",
254
+ body: JSON.stringify(formData)
255
+ });
256
+ await this.fetchRows();
257
+ this.hideUpdateForm();
258
+ } catch (error) {
259
+ console.error(error);
260
+ this.showAlert("Update My Entity Error", error);
261
+ }
236
262
  }
237
- const updateFormDialog = document.getElementById("crud-update-form-dialog");
238
- updateFormDialog.close();
239
- }
240
- {% endif %}
241
263
 
242
- {% if allow_delete %}
243
- async function showDeleteForm(id) {
244
- crudState.deletedRowId = id;
245
- const deleteFormDialog = document.getElementById("crud-delete-form-dialog");
246
- const deleteForm = document.getElementById("crud-delete-form");
247
- result = await UTIL.fetchAPI(`${apiUrl}/${id}`, { method: "GET" });
248
- UTIL.setFormData(deleteForm, result);
249
- deleteFormDialog.showModal();
250
- }
264
+ hideUpdateForm(event = null) {
265
+ if (event) event.preventDefault();
266
+ document.getElementById("crud-update-form-dialog").close();
267
+ }
251
268
 
252
- async function deleteRow(event = null) {
253
- if (event != null) {
254
- event.preventDefault();
269
+ // Delete methods
270
+ async showDeleteForm(id) {
271
+ this.state.deletedRowId = id;
272
+ const deleteDialog = document.getElementById("crud-delete-form-dialog");
273
+ const deleteForm = document.getElementById("crud-delete-form");
274
+ const formData = await UTIL.fetchAPI(`${this.apiUrl}/${id}`, { method: "GET" });
275
+ UTIL.setFormData(deleteForm, formData);
276
+ deleteDialog.showModal();
255
277
  }
256
- try {
257
- await UTIL.fetchAPI(`${apiUrl}/${crudState.deletedRowId}`, {method: "DELETE",});
258
- await fetchRows();
259
- hideDeleteForm();
260
- } catch(error) {
261
- showAlert("Delete My Entity Error", error);
278
+
279
+ async deleteRow(event = null) {
280
+ if (event) event.preventDefault();
281
+ try {
282
+ await UTIL.fetchAPI(`${this.apiUrl}/${this.state.deletedRowId}`, { method: "DELETE" });
283
+ await this.fetchRows();
284
+ this.hideDeleteForm();
285
+ } catch (error) {
286
+ console.error(error);
287
+ this.showAlert("Delete My Entity Error", error);
288
+ }
262
289
  }
263
- }
264
290
 
265
- function hideDeleteForm(event = null) {
266
- if (event != null) {
267
- event.preventDefault();
291
+ hideDeleteForm(event = null) {
292
+ if (event) event.preventDefault();
293
+ document.getElementById("crud-delete-form-dialog").close();
268
294
  }
269
- const deleteFormDialog = document.getElementById("crud-delete-form-dialog");
270
- deleteFormDialog.close();
271
- }
272
- {% endif %}
273
295
 
274
- function showAlert(title, error) {
275
- const alertDialog = document.getElementById("crud-alert-dialog");
276
- const alertTitle = document.getElementById("crud-alert-title");
277
- const alertMessage = document.getElementById("crud-alert-message");
278
- const errorMessage = error.message ? error.message : String(error);
279
- alertTitle.textContent = title;
280
- alertMessage.textContent = errorMessage;
281
- alertDialog.showModal();
282
- }
296
+ // Alert methods
297
+ showAlert(title, error) {
298
+ const alertDialog = document.getElementById("crud-alert-dialog");
299
+ document.getElementById("crud-alert-title").textContent = title;
300
+ document.getElementById("crud-alert-message").textContent = error.message || String(error);
301
+ alertDialog.showModal();
302
+ }
283
303
 
284
- function hideAlert(event = null) {
285
- if (event != null) {
286
- event.preventDefault();
304
+ hideAlert(event = null) {
305
+ if (event) event.preventDefault();
306
+ document.getElementById("crud-alert-dialog").close();
287
307
  }
288
- const alertDialog = document.getElementById("crud-alert-dialog");
289
- alertDialog.close();
290
308
  }
291
309
 
310
+ // Initialize the CrudApp on DOM ready
292
311
  document.addEventListener("DOMContentLoaded", () => {
293
- const filterInput = document.getElementById("crud-filter");
294
- filterInput.value = crudState.filter;
295
- fetchRows(crudState.currentPage);
312
+ const app = document.getElementById("crud-app");
313
+ new CrudApp("/api/v1/my-entities", {
314
+ pageSize: JSON.parse(app.dataset.pageSize),
315
+ currentPage: JSON.parse(app.dataset.page),
316
+ sort: JSON.parse(app.dataset.sort),
317
+ filter: JSON.parse(app.dataset.filter),
318
+ allowCreate: JSON.parse(app.dataset.allowCreate),
319
+ allowUpdate: JSON.parse(app.dataset.allowUpdate),
320
+ allowDelete: JSON.parse(app.dataset.allowDelete),
321
+ updatedRowId: null,
322
+ deletedRowId: null,
323
+ });
296
324
  });
297
325
  </script>
@@ -22,7 +22,9 @@ class MyEntityUpdate(SQLModel):
22
22
  my_column: str | None = None
23
23
 
24
24
  def with_audit(self, updated_by: str) -> "MyEntityUpdateWithAudit":
25
- return MyEntityUpdateWithAudit(**self.model_dump(), updated_by=updated_by)
25
+ return MyEntityUpdateWithAudit(
26
+ **self.model_dump(exclude_none=True), updated_by=updated_by
27
+ )
26
28
 
27
29
 
28
30
  class MyEntityUpdateWithAudit(MyEntityUpdate):
@@ -3,6 +3,7 @@ from typing import Any
3
3
 
4
4
  import ulid
5
5
  from my_app_name.common.base_db_repository import BaseDBRepository
6
+ from my_app_name.common.error import InvalidValueError
6
7
  from my_app_name.module.auth.service.role.repository.role_repository import (
7
8
  RoleRepository,
8
9
  )
@@ -65,6 +66,20 @@ class RoleDBRepository(
65
66
  for data in role_map.values()
66
67
  ]
67
68
 
69
+ async def validate_permission_names(self, permission_names: list[str]):
70
+ async with self._session_scope() as session:
71
+ result = await self._execute_statement(
72
+ session,
73
+ select(Permission.name).where(Permission.name.in_(permission_names)),
74
+ )
75
+ existing_permissions = {row[0] for row in result.all()}
76
+ # Identify any missing permission names
77
+ missing_permissions = set(permission_names) - existing_permissions
78
+ if missing_permissions:
79
+ raise InvalidValueError(
80
+ f"Permission(s) not found: {', '.join(missing_permissions)}"
81
+ )
82
+
68
83
  async def add_permissions(self, data: dict[str, list[str]], created_by: str):
69
84
  now = datetime.datetime.now(datetime.timezone.utc)
70
85
  # get mapping from perrmission names to permission ids
@@ -94,6 +109,8 @@ class RoleDBRepository(
94
109
  )
95
110
  )
96
111
  )
112
+ if len(data_dict_list) == 0:
113
+ return
97
114
  # Insert rolePermissions
98
115
  async with self._session_scope() as session:
99
116
  await self._execute_statement(
@@ -104,5 +121,5 @@ class RoleDBRepository(
104
121
  async with self._session_scope() as session:
105
122
  await self._execute_statement(
106
123
  session,
107
- delete(RolePermission).where(RolePermission.role_id._in(role_ids)),
124
+ delete(RolePermission).where(RolePermission.role_id.in_(role_ids)),
108
125
  )
@@ -18,6 +18,10 @@ class RoleRepository(ABC):
18
18
  async def get_by_ids(self, id_list: list[str]) -> RoleResponse:
19
19
  """Get roles by ids"""
20
20
 
21
+ @abstractmethod
22
+ async def validate_permission_names(self, permission_names: list[str]):
23
+ """Validate Permission names"""
24
+
21
25
  @abstractmethod
22
26
  async def add_permissions(self, data: dict[str, list[str]], created_by: str):
23
27
  """Adding permissions to roles"""
@@ -50,13 +50,17 @@ class RoleService(BaseService):
50
50
  async def create_role_bulk(
51
51
  self, data: list[RoleCreateWithPermissionsAndAudit]
52
52
  ) -> list[RoleResponse]:
53
- permission_names = [row.get_permission_names() for row in data]
54
- data = [row.get_role_create_with_audit() for row in data]
55
- roles = await self.role_repository.create_bulk(data)
53
+ bulk_permission_names = [row.get_permission_names() for row in data]
54
+ for permission_names in bulk_permission_names:
55
+ await self.role_repository.validate_permission_names(permission_names)
56
+ bulk_role_data = [row.get_role_create_with_audit() for row in data]
57
+ roles = await self.role_repository.create_bulk(bulk_role_data)
56
58
  if len(roles) > 0:
57
59
  created_by = roles[0].created_by
58
60
  await self.role_repository.add_permissions(
59
- data={role.id: permission_names[i] for i, role in enumerate(roles)},
61
+ data={
62
+ role.id: bulk_permission_names[i] for i, role in enumerate(roles)
63
+ },
60
64
  created_by=created_by,
61
65
  )
62
66
  return await self.role_repository.get_by_ids([role.id for role in roles])
@@ -70,8 +74,9 @@ class RoleService(BaseService):
70
74
  self, data: RoleCreateWithPermissionsAndAudit
71
75
  ) -> RoleResponse:
72
76
  permission_names = data.get_permission_names()
73
- data = data.get_role_create_with_audit()
74
- role = await self.role_repository.create(data)
77
+ await self.role_repository.validate_permission_names(permission_names)
78
+ role_data = data.get_role_create_with_audit()
79
+ role = await self.role_repository.create(role_data)
75
80
  await self.role_repository.add_permissions(
76
81
  data={role.id: permission_names}, created_by=role.created_by
77
82
  )
@@ -85,15 +90,18 @@ class RoleService(BaseService):
85
90
  async def update_role_bulk(
86
91
  self, role_ids: list[str], data: RoleUpdateWithPermissionsAndAudit
87
92
  ) -> list[RoleResponse]:
88
- permission_names = [row.get_permission_names() for row in data]
89
- data = [row.get_role_update_with_audit() for row in data]
90
- await self.role_repository.update_bulk(role_ids, data)
93
+ bulk_permission_names = [row.get_permission_names() for row in data]
94
+ for permission_names in bulk_permission_names:
95
+ await self.role_repository.validate_permission_names(permission_names)
96
+ bulk_role_data = [row.get_role_update_with_audit() for row in data]
97
+ await self.role_repository.update_bulk(role_ids, bulk_role_data)
91
98
  if len(role_ids) > 0:
92
- updated_by = data[0].updated_by
99
+ updated_by = bulk_role_data[0].updated_by
93
100
  await self.role_repository.remove_all_permissions(role_ids)
94
101
  await self.role_repository.add_permissions(
95
102
  data={
96
- role_id: permission_names[i] for i, role_id in enumerate(role_ids)
103
+ role_id: bulk_permission_names[i]
104
+ for i, role_id in enumerate(role_ids)
97
105
  },
98
106
  created_by=updated_by,
99
107
  )
@@ -108,6 +116,7 @@ class RoleService(BaseService):
108
116
  self, role_id: str, data: RoleUpdateWithPermissionsAndAudit
109
117
  ) -> RoleResponse:
110
118
  permission_names = data.get_permission_names()
119
+ await self.role_repository.validate_permission_names(permission_names)
111
120
  role_data = data.get_role_update_with_audit()
112
121
  await self.role_repository.update(role_id, role_data)
113
122
  await self.role_repository.remove_all_permissions([role_id])
@@ -3,7 +3,7 @@ from typing import Any
3
3
 
4
4
  import ulid
5
5
  from my_app_name.common.base_db_repository import BaseDBRepository
6
- from my_app_name.common.error import NotFoundError, UnauthorizedError
6
+ from my_app_name.common.error import InvalidValueError, NotFoundError, UnauthorizedError
7
7
  from my_app_name.module.auth.service.user.repository.user_repository import (
8
8
  UserRepository,
9
9
  )
@@ -87,6 +87,19 @@ class UserDBRepository(
87
87
  for data in user_map.values()
88
88
  ]
89
89
 
90
+ async def validate_role_names(self, role_names: list[str]):
91
+ async with self._session_scope() as session:
92
+ result = await self._execute_statement(
93
+ session, select(Role.name).where(Role.name.in_(role_names))
94
+ )
95
+ existing_roles = {row[0] for row in result.all()}
96
+ # Identify any missing role names
97
+ missing_roles = set(role_names) - existing_roles
98
+ if missing_roles:
99
+ raise InvalidValueError(
100
+ f"Role(s) not found: {', '.join(missing_roles)}"
101
+ )
102
+
90
103
  async def add_roles(self, data: dict[str, list[str]], created_by: str):
91
104
  now = datetime.datetime.now(datetime.timezone.utc)
92
105
  # get mapping from role names to role ids
@@ -111,6 +124,8 @@ class UserDBRepository(
111
124
  )
112
125
  )
113
126
  )
127
+ if len(data_dict_list) == 0:
128
+ return
114
129
  async with self._session_scope() as session:
115
130
  await self._execute_statement(
116
131
  session, insert(UserRole).values(data_dict_list)
@@ -120,7 +135,7 @@ class UserDBRepository(
120
135
  async with self._session_scope() as session:
121
136
  await self._execute_statement(
122
137
  session,
123
- delete(UserRole).where(UserRole.user_id._in(user_ids)),
138
+ delete(UserRole).where(UserRole.user_id.in_(user_ids)),
124
139
  )
125
140
 
126
141
  async def get_by_credentials(self, username: str, password: str) -> UserResponse: