@nest-util/nest-crud 0.1.1 → 1.0.1

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 (63) hide show
  1. package/README.md +12 -4
  2. package/dist/index.d.ts +11 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +11 -4
  5. package/dist/lib/controllers/nest-crud.controller.d.ts +2 -1
  6. package/dist/lib/controllers/nest-crud.controller.d.ts.map +1 -1
  7. package/dist/lib/controllers/nest-crud.controller.js +41 -5
  8. package/dist/lib/decorators/audit-log.decorator.d.ts +7 -0
  9. package/dist/lib/decorators/audit-log.decorator.d.ts.map +1 -0
  10. package/dist/lib/decorators/audit-log.decorator.js +7 -0
  11. package/dist/lib/dtos/cursor-pagination.dto.d.ts +6 -0
  12. package/dist/lib/dtos/cursor-pagination.dto.d.ts.map +1 -0
  13. package/dist/lib/dtos/cursor-pagination.dto.js +47 -0
  14. package/dist/lib/dtos/list-audit-logs.dto.d.ts +8 -0
  15. package/dist/lib/dtos/list-audit-logs.dto.d.ts.map +1 -0
  16. package/dist/lib/dtos/list-audit-logs.dto.js +65 -0
  17. package/dist/lib/entities/audit-log.entity.d.ts +13 -0
  18. package/dist/lib/entities/audit-log.entity.d.ts.map +1 -0
  19. package/dist/lib/entities/audit-log.entity.js +58 -0
  20. package/dist/lib/helpers/cursor-pagination.helper.d.ts +9 -0
  21. package/dist/lib/helpers/cursor-pagination.helper.d.ts.map +1 -0
  22. package/dist/lib/helpers/cursor-pagination.helper.js +93 -0
  23. package/dist/lib/interceptors/audit-log.interceptor.d.ts +12 -0
  24. package/dist/lib/interceptors/audit-log.interceptor.d.ts.map +1 -0
  25. package/dist/lib/interceptors/audit-log.interceptor.js +72 -0
  26. package/dist/lib/interfaces/audit-log.interface.d.ts +11 -0
  27. package/dist/lib/interfaces/audit-log.interface.d.ts.map +1 -0
  28. package/dist/lib/interfaces/audit-log.interface.js +2 -0
  29. package/dist/lib/interfaces/crud.interface.d.ts +16 -1
  30. package/dist/lib/interfaces/crud.interface.d.ts.map +1 -1
  31. package/dist/lib/interfaces/cursor-strategy.interface.d.ts +30 -0
  32. package/dist/lib/interfaces/cursor-strategy.interface.d.ts.map +1 -0
  33. package/dist/lib/interfaces/cursor-strategy.interface.js +2 -0
  34. package/dist/lib/interfaces/find-mine.interface.d.ts +6 -0
  35. package/dist/lib/interfaces/find-mine.interface.d.ts.map +1 -0
  36. package/dist/lib/interfaces/find-mine.interface.js +2 -0
  37. package/dist/lib/interfaces/hooks.interface.d.ts +52 -0
  38. package/dist/lib/interfaces/hooks.interface.d.ts.map +1 -0
  39. package/dist/lib/interfaces/hooks.interface.js +2 -0
  40. package/dist/lib/nest-crud.module.d.ts.map +1 -1
  41. package/dist/lib/nest-crud.module.js +6 -3
  42. package/dist/lib/services/audit-log.service.d.ts +43 -0
  43. package/dist/lib/services/audit-log.service.d.ts.map +1 -0
  44. package/dist/lib/services/audit-log.service.js +154 -0
  45. package/dist/lib/services/nest-crud.service.d.ts +23 -4
  46. package/dist/lib/services/nest-crud.service.d.ts.map +1 -1
  47. package/dist/lib/services/nest-crud.service.js +163 -8
  48. package/dist/lib/testing/crud-controller.test-suites.d.ts +4 -0
  49. package/dist/lib/testing/crud-controller.test-suites.d.ts.map +1 -0
  50. package/dist/lib/testing/crud-controller.test-suites.js +165 -0
  51. package/dist/lib/testing/crud-service.test-suites.d.ts +4 -0
  52. package/dist/lib/testing/crud-service.test-suites.d.ts.map +1 -0
  53. package/dist/lib/testing/crud-service.test-suites.js +279 -0
  54. package/dist/lib/testing/index.d.ts +5 -0
  55. package/dist/lib/testing/index.d.ts.map +1 -0
  56. package/dist/lib/testing/index.js +7 -0
  57. package/dist/lib/testing/mock-repository.d.ts +7 -0
  58. package/dist/lib/testing/mock-repository.d.ts.map +1 -0
  59. package/dist/lib/testing/mock-repository.js +97 -0
  60. package/dist/lib/testing/testing.interface.d.ts +52 -0
  61. package/dist/lib/testing/testing.interface.d.ts.map +1 -0
  62. package/dist/lib/testing/testing.interface.js +2 -0
  63. package/package.json +21 -5
@@ -3,9 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.NestCrudService = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const common_1 = require("@nestjs/common");
6
- const nest_audit_1 = require("@nest-util/nest-audit");
6
+ const audit_log_entity_1 = require("../entities/audit-log.entity");
7
7
  const filter_helper_1 = require("../helpers/filter.helper");
8
8
  const pagination_helper_1 = require("../helpers/pagination.helper");
9
+ const cursor_pagination_helper_1 = require("../helpers/cursor-pagination.helper");
9
10
  let NestCrudService = class NestCrudService {
10
11
  constructor(options) {
11
12
  this.repo = options.repository;
@@ -17,6 +18,12 @@ let NestCrudService = class NestCrudService {
17
18
  this.createDtoClass = options.createDtoClass;
18
19
  this.updateDtoClass = options.updateDtoClass;
19
20
  this.disabledEndpoints = options.disabledEndpoints ?? [];
21
+ this.cursorStrategy =
22
+ options.cursorStrategy ?? (0, cursor_pagination_helper_1.detectCursorStrategy)(this.repo);
23
+ this.hooks = options.hooks ?? {};
24
+ this.transactionConfig = options.transactionConfig ?? {};
25
+ this.userOwnershipField = options.userOwnershipField;
26
+ this.findMineQuery = options.findMineQuery;
20
27
  }
21
28
  async resolveRelations(payload) {
22
29
  if (!this.relations.length)
@@ -39,6 +46,32 @@ let NestCrudService = class NestCrudService {
39
46
  }
40
47
  return payload;
41
48
  }
49
+ async executeInTransaction(fn, isolationLevel) {
50
+ const queryRunner = this.repo.manager.connection.createQueryRunner();
51
+ await queryRunner.startTransaction(isolationLevel);
52
+ try {
53
+ const result = await fn();
54
+ await queryRunner.commitTransaction();
55
+ return result;
56
+ }
57
+ catch (error) {
58
+ await queryRunner.rollbackTransaction();
59
+ throw error;
60
+ }
61
+ finally {
62
+ await queryRunner.release();
63
+ }
64
+ }
65
+ async executeHook(hook, context) {
66
+ if (!hook)
67
+ return;
68
+ if (hook.transaction) {
69
+ await this.executeInTransaction(() => hook.handler(context), this.transactionConfig?.isolationLevel);
70
+ }
71
+ else {
72
+ await hook.handler(context);
73
+ }
74
+ }
42
75
  async findAll(query) {
43
76
  const qb = this.repo.createQueryBuilder('e');
44
77
  if (this.include.length > 0) {
@@ -72,7 +105,115 @@ let NestCrudService = class NestCrudService {
72
105
  ? { data, meta: { ...paginationMeta, total } }
73
106
  : { data };
74
107
  }
108
+ async findMine(userId, query) {
109
+ if (!this.userOwnershipField && !this.findMineQuery) {
110
+ throw new common_1.BadRequestException('findMine not configured');
111
+ }
112
+ const qb = this.repo.createQueryBuilder('e');
113
+ if (this.findMineQuery) {
114
+ this.findMineQuery(qb, userId);
115
+ }
116
+ else if (this.userOwnershipField) {
117
+ qb.where(`e.${String(this.userOwnershipField)} = :userId`, { userId });
118
+ }
119
+ if (this.include.length > 0) {
120
+ this.include.forEach((relation) => {
121
+ const parts = relation.split('.');
122
+ if (parts.length === 1) {
123
+ qb.leftJoinAndSelect(`e.${parts[0]}`, parts[0]);
124
+ }
125
+ else {
126
+ const parentAlias = parts.slice(0, -1).join('_');
127
+ const field = parts[parts.length - 1];
128
+ const alias = parts.join('_');
129
+ qb.leftJoinAndSelect(`${parentAlias}.${field}`, alias);
130
+ }
131
+ });
132
+ }
133
+ (0, filter_helper_1.applyFilters)(qb, query.filter, this.allowedFilters);
134
+ const paginationMeta = (0, pagination_helper_1.applyPagination)(qb, query);
135
+ if (query.orderBy) {
136
+ const orderDirection = query.orderDirection === 'ASC' ? 'ASC' : 'DESC';
137
+ if (this.allowedSortFields.length === 0 ||
138
+ this.allowedSortFields.includes(query.orderBy)) {
139
+ qb.orderBy(`e.${query.orderBy}`, orderDirection);
140
+ }
141
+ }
142
+ const [entities, total] = await qb.getManyAndCount();
143
+ const data = this.toResponseDto
144
+ ? this.toResponseDto(entities)
145
+ : entities;
146
+ return paginationMeta
147
+ ? { data, meta: { ...paginationMeta, total } }
148
+ : { data };
149
+ }
150
+ async findAllWithCursor(query) {
151
+ const limit = query.limit ?? 10;
152
+ const strategy = this.cursorStrategy;
153
+ const orderDirection = 'DESC';
154
+ const qb = this.repo.createQueryBuilder('e');
155
+ // Join relations
156
+ if (this.include.length > 0) {
157
+ this.include.forEach((relation) => {
158
+ const parts = relation.split('.');
159
+ if (parts.length === 1) {
160
+ qb.leftJoinAndSelect(`e.${parts[0]}`, parts[0]);
161
+ }
162
+ else {
163
+ const parentAlias = parts.slice(0, -1).join('_');
164
+ const field = parts[parts.length - 1];
165
+ const alias = parts.join('_');
166
+ qb.leftJoinAndSelect(`${parentAlias}.${field}`, alias);
167
+ }
168
+ });
169
+ }
170
+ // Apply filters
171
+ (0, filter_helper_1.applyFilters)(qb, query.filter, this.allowedFilters);
172
+ // Apply cursor filter
173
+ if (query.cursor) {
174
+ const decoded = (0, cursor_pagination_helper_1.decodeCursor)(query.cursor, strategy);
175
+ (0, cursor_pagination_helper_1.applyCursorFilter)(qb, decoded, strategy, orderDirection);
176
+ }
177
+ // Default ordering by id
178
+ qb.orderBy(`e.id`, orderDirection);
179
+ // Fetch limit + 1 to detect hasMore
180
+ const take = limit + 1;
181
+ const entities = await qb.take(take).getMany();
182
+ const hasMore = entities.length > limit;
183
+ const data = hasMore ? entities.slice(0, limit) : entities;
184
+ // Build next cursor from last entity
185
+ const nextCursor = hasMore ? (0, cursor_pagination_helper_1.buildNextCursor)(data, strategy) : null;
186
+ // Optionally compute total count
187
+ let total;
188
+ if (query.includeTotal) {
189
+ // Build a clean count query (reuse same filters but no cursor/order/take)
190
+ const countQb = this.repo.createQueryBuilder('e');
191
+ if (this.include.length > 0) {
192
+ this.include.forEach((relation) => {
193
+ const parts = relation.split('.');
194
+ if (parts.length === 1) {
195
+ countQb.leftJoin(`e.${parts[0]}`, parts[0]);
196
+ }
197
+ });
198
+ }
199
+ (0, filter_helper_1.applyFilters)(countQb, query.filter, this.allowedFilters);
200
+ total = await countQb.getCount();
201
+ }
202
+ const response = this.toResponseDto
203
+ ? this.toResponseDto(data)
204
+ : data;
205
+ return {
206
+ data: response,
207
+ meta: {
208
+ limit,
209
+ hasMore,
210
+ nextCursor,
211
+ ...(total !== undefined ? { total } : {}),
212
+ },
213
+ };
214
+ }
75
215
  async findOne(id) {
216
+ await this.executeHook(this.hooks.beforeFindOne, { id });
76
217
  const entity = await this.repo.findOne({
77
218
  where: { id },
78
219
  relations: this.include,
@@ -80,16 +221,21 @@ let NestCrudService = class NestCrudService {
80
221
  if (!entity) {
81
222
  throw new common_1.NotFoundException('Resource not found');
82
223
  }
83
- return this.toResponseDto
224
+ const result = this.toResponseDto
84
225
  ? this.toResponseDto(entity)
85
226
  : entity;
227
+ await this.executeHook(this.hooks.afterFindOne, { entity, id });
228
+ return result;
86
229
  }
87
230
  async create(payload) {
231
+ await this.executeHook(this.hooks.beforeCreate, { payload });
88
232
  const resolved = await this.resolveRelations(payload);
89
233
  const entity = await this.repo.save(resolved);
90
- return this.toResponseDto
234
+ const result = this.toResponseDto
91
235
  ? this.toResponseDto(entity)
92
236
  : entity;
237
+ await this.executeHook(this.hooks.afterCreate, { entity, payload });
238
+ return result;
93
239
  }
94
240
  async update(id, payload) {
95
241
  const existing = await this.repo.findOneBy({
@@ -98,23 +244,32 @@ let NestCrudService = class NestCrudService {
98
244
  if (!existing) {
99
245
  throw new common_1.NotFoundException('Resource not found');
100
246
  }
247
+ await this.executeHook(this.hooks.beforeUpdate, { payload, entity: existing, id });
101
248
  const resolved = await this.resolveRelations(payload);
102
249
  this.repo.merge(existing, resolved);
103
250
  await this.repo.save(existing);
104
- return this.findOne(id);
251
+ const result = await this.findOne(id);
252
+ await this.executeHook(this.hooks.afterUpdate, { entity: result, payload, id });
253
+ return result;
105
254
  }
106
255
  async remove(id) {
107
- const result = await this.repo.delete(id);
108
- if (result.affected === 0) {
256
+ const existing = await this.repo.findOneBy({
257
+ id,
258
+ });
259
+ if (!existing) {
109
260
  throw new common_1.NotFoundException('Resource not found');
110
261
  }
111
- return true;
262
+ await this.executeHook(this.hooks.beforeRemove, { entity: existing, id });
263
+ const result = await this.repo.delete(id);
264
+ const deleted = result.affected !== 0;
265
+ await this.executeHook(this.hooks.afterRemove, { id, deleted });
266
+ return deleted;
112
267
  }
113
268
  async findAuditLogs(query) {
114
269
  const page = query.page ?? 1;
115
270
  const limit = query.limit ?? 10;
116
271
  const qb = this.repo.manager
117
- .getRepository(nest_audit_1.AuditLogEntity)
272
+ .getRepository(audit_log_entity_1.AuditLogEntity)
118
273
  .createQueryBuilder('auditLog')
119
274
  .where('auditLog.entity = :entity', {
120
275
  entity: this.repo.metadata.name,
@@ -0,0 +1,4 @@
1
+ import { ObjectLiteral } from 'typeorm';
2
+ import { CrudControllerTestConfig, CrudTestContext } from './testing.interface';
3
+ export declare function crudControllerTests<TEntity extends ObjectLiteral, TCreateDto, TUpdateDto, TResponseDto>(config: CrudControllerTestConfig<TEntity, TCreateDto, TUpdateDto, TResponseDto>): CrudTestContext;
4
+ //# sourceMappingURL=crud-controller.test-suites.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crud-controller.test-suites.d.ts","sourceRoot":"","sources":["../../../src/lib/testing/crud-controller.test-suites.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAMxC,OAAO,EAAE,wBAAwB,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAGhF,wBAAgB,mBAAmB,CACjC,OAAO,SAAS,aAAa,EAC7B,UAAU,EACV,UAAU,EACV,YAAY,EAEZ,MAAM,EAAE,wBAAwB,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,CAAC,GAC9E,eAAe,CAoNjB"}
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.crudControllerTests = crudControllerTests;
4
+ const testing_1 = require("@nestjs/testing");
5
+ const common_1 = require("@nestjs/common");
6
+ const nest_crud_controller_1 = require("../controllers/nest-crud.controller");
7
+ const mock_repository_1 = require("./mock-repository");
8
+ function crudControllerTests(config) {
9
+ let controller;
10
+ let service;
11
+ let ControllerBase;
12
+ let moduleRef;
13
+ beforeEach(async () => {
14
+ service = {
15
+ disabledEndpoints: [],
16
+ findAll: jest.fn(),
17
+ findOne: jest.fn(),
18
+ create: jest.fn(),
19
+ update: jest.fn(),
20
+ remove: jest.fn(),
21
+ findAuditLogs: jest.fn(),
22
+ findMine: jest.fn(),
23
+ };
24
+ ControllerBase = config.controllerFactory();
25
+ class TestController extends ControllerBase {
26
+ constructor() {
27
+ super(service);
28
+ }
29
+ }
30
+ moduleRef = await testing_1.Test.createTestingModule({
31
+ controllers: [TestController],
32
+ }).compile();
33
+ controller = moduleRef.get(TestController);
34
+ });
35
+ describe('findAll', () => {
36
+ it('should call service.findAll with query params', async () => {
37
+ const query = { page: 1, limit: 10 };
38
+ const expected = { data: [], meta: { total: 0 } };
39
+ service.findAll.mockResolvedValue(expected);
40
+ const result = await controller.findAll(query);
41
+ expect(service.findAll).toHaveBeenCalledWith(query);
42
+ expect(result).toEqual(expected);
43
+ });
44
+ it('should handle pagination parameters', async () => {
45
+ const query = { page: 5, limit: 100 };
46
+ service.findAll.mockResolvedValue({ data: [] });
47
+ await controller.findAll(query);
48
+ expect(service.findAll).toHaveBeenCalledWith(expect.objectContaining({ page: 5, limit: 100 }));
49
+ });
50
+ it('should handle complex filter objects', async () => {
51
+ const query = {
52
+ filter: { name_cont: 'Alice', isActive_eq: 'true' },
53
+ };
54
+ service.findAll.mockResolvedValue({ data: [] });
55
+ await controller.findAll(query);
56
+ expect(service.findAll).toHaveBeenCalledWith(expect.objectContaining({
57
+ filter: { name_cont: 'Alice', isActive_eq: 'true' },
58
+ }));
59
+ });
60
+ it('should work with no query parameters', async () => {
61
+ service.findAll.mockResolvedValue({ data: [] });
62
+ await controller.findAll({});
63
+ expect(service.findAll).toHaveBeenCalledWith({});
64
+ });
65
+ });
66
+ describe('findOne', () => {
67
+ it('should call service.findOne with correct id', async () => {
68
+ const expectedResult = { id: 1, name: 'Test' };
69
+ service.findOne.mockResolvedValue(expectedResult);
70
+ const result = await controller.findOne(1);
71
+ expect(service.findOne).toHaveBeenCalledWith(1);
72
+ expect(result).toEqual(expectedResult);
73
+ });
74
+ });
75
+ describe('create', () => {
76
+ it('should call service.create with dto', async () => {
77
+ const dto = config.test.createPayload;
78
+ const expectedResult = { id: 1 };
79
+ service.create.mockResolvedValue(expectedResult);
80
+ const result = await controller.create(dto);
81
+ expect(service.create).toHaveBeenCalledWith(dto);
82
+ expect(result).toEqual(expectedResult);
83
+ });
84
+ });
85
+ describe('update', () => {
86
+ it('should call service.update with id and dto', async () => {
87
+ const dto = config.test.updatePayload;
88
+ const expectedResult = { id: 1 };
89
+ service.update.mockResolvedValue(expectedResult);
90
+ const result = await controller.update(1, dto);
91
+ expect(service.update).toHaveBeenCalledWith(1, dto);
92
+ expect(result).toEqual(expectedResult);
93
+ });
94
+ });
95
+ describe('remove', () => {
96
+ it('should call service.remove', async () => {
97
+ service.remove.mockResolvedValue(true);
98
+ const result = await controller.remove(1);
99
+ expect(service.remove).toHaveBeenCalledWith(1);
100
+ expect(result).toBe(true);
101
+ });
102
+ });
103
+ describe('findMine', () => {
104
+ it('should call service.findMine with user id', async () => {
105
+ const query = { page: 1, limit: 10 };
106
+ const expected = { data: [] };
107
+ service.findMine.mockResolvedValue(expected);
108
+ const mockUser = { id: 42 };
109
+ const result = await controller.findMine(mockUser, query);
110
+ expect(service.findMine).toHaveBeenCalledWith(42, query);
111
+ expect(result).toEqual(expected);
112
+ });
113
+ });
114
+ describe('disabledEndpoints', () => {
115
+ it('should block disabled endpoints', async () => {
116
+ service.disabledEndpoints = ['create'];
117
+ expect(() => controller.create({})).toThrow(common_1.NotFoundException);
118
+ expect(service.create).not.toHaveBeenCalled();
119
+ });
120
+ });
121
+ describe('findAuditLogs', () => {
122
+ it('should call service.findAuditLogs with query params', async () => {
123
+ const query = { page: 1, limit: 5, user_id: '1' };
124
+ const expected = { data: [], meta: { total: 0, page: 1, limit: 5 } };
125
+ service.findAuditLogs.mockResolvedValue(expected);
126
+ const result = await controller.findAuditLogs?.(query);
127
+ expect(service.findAuditLogs).toHaveBeenCalledWith(query);
128
+ expect(result).toEqual(expected);
129
+ });
130
+ it('should throw when audit logs endpoint is not available', () => {
131
+ service.findAuditLogs = undefined;
132
+ expect(() => controller.findAuditLogs?.({})).toThrow(common_1.NotFoundException);
133
+ });
134
+ });
135
+ describe('permissions', () => {
136
+ it('should attach auth permission metadata to configured handlers', () => {
137
+ if (!config.permissions)
138
+ return;
139
+ const PermissionedBase = config.controllerFactory();
140
+ class PermissionedCtrl extends PermissionedBase {
141
+ constructor() {
142
+ super(service);
143
+ }
144
+ }
145
+ for (const [endpoint, permission] of Object.entries(config.permissions)) {
146
+ const method = PermissionedCtrl.prototype[endpoint];
147
+ if (!method)
148
+ continue;
149
+ const metadata = Reflect.getMetadata(nest_crud_controller_1.AUTH_PERMISSIONS_METADATA_KEY, method);
150
+ const expected = Array.isArray(permission)
151
+ ? permission
152
+ : [permission];
153
+ expect(metadata).toEqual(expected);
154
+ }
155
+ });
156
+ });
157
+ const ctx = {
158
+ get module() {
159
+ return moduleRef;
160
+ },
161
+ repository: null,
162
+ createMockQb: mock_repository_1.createMockQb,
163
+ };
164
+ return ctx;
165
+ }
@@ -0,0 +1,4 @@
1
+ import { ObjectLiteral } from 'typeorm';
2
+ import { CrudTestConfig, CrudTestContext } from './testing.interface';
3
+ export declare function crudServiceTests<TEntity extends ObjectLiteral, TCreateDto, TUpdateDto, TResponseDto>(config: CrudTestConfig<TEntity, TCreateDto, TUpdateDto, TResponseDto>): CrudTestContext;
4
+ //# sourceMappingURL=crud-service.test-suites.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crud-service.test-suites.d.ts","sourceRoot":"","sources":["../../../src/lib/testing/crud-service.test-suites.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAExC,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAGtE,wBAAgB,gBAAgB,CAC9B,OAAO,SAAS,aAAa,EAC7B,UAAU,EACV,UAAU,EACV,YAAY,EACZ,MAAM,EAAE,cAAc,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,CAAC,GAAG,eAAe,CAqVxF"}