@neohm/nh-cli 1.1.4 → 1.1.5

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": "@neohm/nh-cli",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "description": "nh — Command Line Interface for neohm. NestJS projects",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -85,17 +85,31 @@ export class ${n.controllerClass} {
85
85
  }
86
86
  return `import { Body, Controller, Post } from '@nestjs/common';
87
87
  import { ApiTags } from '@nestjs/swagger';
88
- import { CommonListFilterDto, SqlService } from '@neohm/nestend';
88
+ import {
89
+ AuthService,
90
+ CommonListFilterDto,
91
+ ListFilterService,
92
+ } from '@neohm/nestend';
89
93
  import { ${n.listProcessorClass} } from '../libraries/${n.dot}.list.processor';
90
94
 
91
95
  @ApiTags('${n.kebab}')
92
96
  @Controller('api/v1/${n.kebab}')
93
97
  export class ${n.controllerClass} {
94
- constructor(private readonly sqlService: SqlService) {}
98
+ constructor(
99
+ private readonly listFilterService: ListFilterService,
100
+ private readonly authService: AuthService,
101
+ ) {}
95
102
 
96
103
  @Post('list')
97
104
  async list(@Body() body: CommonListFilterDto) {
98
- return new ${n.listProcessorClass}(this.sqlService).process(body);
105
+ // Open by default: authenticate explicitly. validateAccess() gates the
106
+ // feature on the caller's organization, and guarantees the
107
+ // organization_id the processor scopes on — a tenantless user is
108
+ // refused here rather than reaching the query with a null scope.
109
+ // TODO: replace with the sys_scopes identifier this endpoint sits behind.
110
+ const { user } = await this.authService.validateAccess('add-scope', 'add-role');
111
+
112
+ return new ${n.listProcessorClass}(this.listFilterService, user).process(body);
99
113
  }
100
114
  }
101
115
  `;
@@ -145,27 +159,56 @@ function listProcessor(base, tableName) {
145
159
  CommonListFilterConfig,
146
160
  CommonListFilterDto,
147
161
  CommonListProcessor,
162
+ FilterValueTypeEnum,
163
+ ListFilterService,
148
164
  SortDirection,
149
- SqlService,
165
+ UserEntity,
150
166
  } from '@neohm/nestend';
151
167
 
152
168
  /**
153
- * Lists / filters ${n.pascal}. The \`columns\` config below is the allowlist:
154
- * an identifier must appear here before a caller can filter or sort on it.
155
- * Mark a column \`searchable: true\` to include it in the free-text \`str\` sweep.
169
+ * Lists / filters ${n.pascal}.
170
+ *
171
+ * Two separate surfaces, and it matters which one a rule belongs to:
172
+ *
173
+ * - \`columns\` is the caller's allowlist. An identifier must appear here
174
+ * before a request can filter or sort on it, and \`searchable: true\` adds
175
+ * it to the free-text \`str\` sweep. Everything here is public. Declare a
176
+ * \`type\`: it decides how a caller's value is cast and which operators the
177
+ * column accepts — add \`operators\` to narrow that further.
178
+ *
179
+ * - \`predicates\` and \`applyCustomPredicates()\` are yours. Both compile
180
+ * inside the relation — before any join aggregate or LATERAL runs — and
181
+ * carry no public identifier, so a payload cannot name, weaken or compete
182
+ * with them. Lifecycle and tenant scoping belong here, never in
183
+ * \`columns\`. Standing literal clauses go in \`predicates\`; anything
184
+ * carrying a value goes in the method, where add() binds it.
156
185
  */
157
186
  export class ${n.listProcessorClass} extends CommonListProcessor {
158
187
  protected config: CommonListFilterConfig = {
159
188
  query: '${tableName} a',
160
189
  columns: [
161
- { key: 'a.id', identifier: 'id' },
162
- { key: 'a.attributes', identifier: 'attributes' },
163
- { key: 'a.created_at', identifier: 'created_at' },
190
+ { key: 'a.id', identifier: 'id', type: FilterValueTypeEnum.UUID },
191
+ {
192
+ key: 'a.attributes',
193
+ identifier: 'attributes',
194
+ type: FilterValueTypeEnum.JSON,
195
+ },
196
+ {
197
+ key: 'a.created_at',
198
+ identifier: 'created_at',
199
+ type: FilterValueTypeEnum.DATETIME,
200
+ },
164
201
  ],
165
- defaultSort: { field: 'id', direction: SortDirection.DESC },
202
+ // Soft-deleted rows are never listed. \`deleted_at\` is deliberately not a
203
+ // declared column: there is nothing a caller should be doing with it.
204
+ predicates: ['a.deleted_at is null'],
205
+ defaultSort: { field: 'created_at', direction: SortDirection.DESC },
166
206
  };
167
207
 
168
- constructor(protected readonly sqlService: SqlService) {
208
+ constructor(
209
+ protected readonly listFilterService: ListFilterService,
210
+ private readonly user: UserEntity,
211
+ ) {
169
212
  super();
170
213
  }
171
214
 
@@ -174,6 +217,14 @@ export class ${n.listProcessorClass} extends CommonListProcessor {
174
217
 
175
218
  return this.handle();
176
219
  }
220
+
221
+ protected applyCustomPredicates() {
222
+ // Filter for specific user's tenant/organization
223
+ this.predicates.add(
224
+ 'a.organization_id = :organization_id',
225
+ this.user.organization_id,
226
+ );
227
+ }
177
228
  }
178
229
  `;
179
230
  }