@adonis-agora/filter 0.0.0-beta-20261110131309
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/CHANGELOG.md +417 -0
- package/LICENSE +21 -0
- package/README.md +75 -0
- package/dist/commands/main.d.ts +19 -0
- package/dist/commands/main.d.ts.map +1 -0
- package/dist/commands/main.js +19 -0
- package/dist/commands/main.js.map +1 -0
- package/dist/commands/make_filter.d.ts +18 -0
- package/dist/commands/make_filter.d.ts.map +1 -0
- package/dist/commands/make_filter.js +32 -0
- package/dist/commands/make_filter.js.map +1 -0
- package/dist/commands/make_filter_client.d.ts +35 -0
- package/dist/commands/make_filter_client.d.ts.map +1 -0
- package/dist/commands/make_filter_client.js +85 -0
- package/dist/commands/make_filter_client.js.map +1 -0
- package/dist/configure.d.ts +15 -0
- package/dist/configure.d.ts.map +1 -0
- package/dist/configure.js +20 -0
- package/dist/configure.js.map +1 -0
- package/dist/providers/filter_provider.d.ts +28 -0
- package/dist/providers/filter_provider.d.ts.map +1 -0
- package/dist/providers/filter_provider.js +33 -0
- package/dist/providers/filter_provider.js.map +1 -0
- package/dist/src/aggregate.d.ts +70 -0
- package/dist/src/aggregate.d.ts.map +1 -0
- package/dist/src/aggregate.js +195 -0
- package/dist/src/aggregate.js.map +1 -0
- package/dist/src/aggregate_path.d.ts +25 -0
- package/dist/src/aggregate_path.d.ts.map +1 -0
- package/dist/src/aggregate_path.js +35 -0
- package/dist/src/aggregate_path.js.map +1 -0
- package/dist/src/apply_from_request.d.ts +85 -0
- package/dist/src/apply_from_request.d.ts.map +1 -0
- package/dist/src/apply_from_request.js +164 -0
- package/dist/src/apply_from_request.js.map +1 -0
- package/dist/src/base_filter.d.ts +49 -0
- package/dist/src/base_filter.d.ts.map +1 -0
- package/dist/src/base_filter.js +33 -0
- package/dist/src/base_filter.js.map +1 -0
- package/dist/src/base_model_filter.d.ts +90 -0
- package/dist/src/base_model_filter.d.ts.map +1 -0
- package/dist/src/base_model_filter.js +85 -0
- package/dist/src/base_model_filter.js.map +1 -0
- package/dist/src/coerce_value.d.ts +26 -0
- package/dist/src/coerce_value.d.ts.map +1 -0
- package/dist/src/coerce_value.js +82 -0
- package/dist/src/coerce_value.js.map +1 -0
- package/dist/src/cursor.d.ts +92 -0
- package/dist/src/cursor.d.ts.map +1 -0
- package/dist/src/cursor.js +107 -0
- package/dist/src/cursor.js.map +1 -0
- package/dist/src/custom_filter.d.ts +36 -0
- package/dist/src/custom_filter.d.ts.map +1 -0
- package/dist/src/custom_filter.js +127 -0
- package/dist/src/custom_filter.js.map +1 -0
- package/dist/src/decorator_metadata.d.ts +41 -0
- package/dist/src/decorator_metadata.d.ts.map +1 -0
- package/dist/src/decorator_metadata.js +83 -0
- package/dist/src/decorator_metadata.js.map +1 -0
- package/dist/src/decorators.d.ts +73 -0
- package/dist/src/decorators.d.ts.map +1 -0
- package/dist/src/decorators.js +118 -0
- package/dist/src/decorators.js.map +1 -0
- package/dist/src/escape-like.d.ts +10 -0
- package/dist/src/escape-like.d.ts.map +1 -0
- package/dist/src/escape-like.js +15 -0
- package/dist/src/escape-like.js.map +1 -0
- package/dist/src/field_aliases.d.ts +30 -0
- package/dist/src/field_aliases.d.ts.map +1 -0
- package/dist/src/field_aliases.js +65 -0
- package/dist/src/field_aliases.js.map +1 -0
- package/dist/src/filter_class.d.ts +46 -0
- package/dist/src/filter_class.d.ts.map +1 -0
- package/dist/src/filter_class.js +244 -0
- package/dist/src/filter_class.js.map +1 -0
- package/dist/src/filter_spec.d.ts +231 -0
- package/dist/src/filter_spec.d.ts.map +1 -0
- package/dist/src/filter_spec.js +182 -0
- package/dist/src/filter_spec.js.map +1 -0
- package/dist/src/filterable_mixin.d.ts +49 -0
- package/dist/src/filterable_mixin.d.ts.map +1 -0
- package/dist/src/filterable_mixin.js +47 -0
- package/dist/src/filterable_mixin.js.map +1 -0
- package/dist/src/generate_client.d.ts +107 -0
- package/dist/src/generate_client.d.ts.map +1 -0
- package/dist/src/generate_client.js +164 -0
- package/dist/src/generate_client.js.map +1 -0
- package/dist/src/group_by_count.d.ts +67 -0
- package/dist/src/group_by_count.d.ts.map +1 -0
- package/dist/src/group_by_count.js +71 -0
- package/dist/src/group_by_count.js.map +1 -0
- package/dist/src/in_memory.d.ts +279 -0
- package/dist/src/in_memory.d.ts.map +1 -0
- package/dist/src/in_memory.js +810 -0
- package/dist/src/in_memory.js.map +1 -0
- package/dist/src/index.d.ts +51 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +33 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lucid_adapter.d.ts +311 -0
- package/dist/src/lucid_adapter.d.ts.map +1 -0
- package/dist/src/lucid_adapter.js +469 -0
- package/dist/src/lucid_adapter.js.map +1 -0
- package/dist/src/lucid_macros.d.ts +87 -0
- package/dist/src/lucid_macros.d.ts.map +1 -0
- package/dist/src/lucid_macros.js +67 -0
- package/dist/src/lucid_macros.js.map +1 -0
- package/dist/src/normalizer.d.ts +21 -0
- package/dist/src/normalizer.d.ts.map +1 -0
- package/dist/src/normalizer.js +61 -0
- package/dist/src/normalizer.js.map +1 -0
- package/dist/src/operators.d.ts +37 -0
- package/dist/src/operators.d.ts.map +1 -0
- package/dist/src/operators.js +43 -0
- package/dist/src/operators.js.map +1 -0
- package/dist/src/paginate.d.ts +12 -0
- package/dist/src/paginate.d.ts.map +1 -0
- package/dist/src/paginate.js +88 -0
- package/dist/src/paginate.js.map +1 -0
- package/dist/src/pagination_stash.d.ts +4 -0
- package/dist/src/pagination_stash.d.ts.map +1 -0
- package/dist/src/pagination_stash.js +19 -0
- package/dist/src/pagination_stash.js.map +1 -0
- package/dist/src/parse_request.d.ts +50 -0
- package/dist/src/parse_request.d.ts.map +1 -0
- package/dist/src/parse_request.js +220 -0
- package/dist/src/parse_request.js.map +1 -0
- package/dist/src/runner.d.ts +65 -0
- package/dist/src/runner.d.ts.map +1 -0
- package/dist/src/runner.js +371 -0
- package/dist/src/runner.js.map +1 -0
- package/dist/src/source_resolver.d.ts +18 -0
- package/dist/src/source_resolver.d.ts.map +1 -0
- package/dist/src/source_resolver.js +64 -0
- package/dist/src/source_resolver.js.map +1 -0
- package/dist/src/spatie_parser.d.ts +39 -0
- package/dist/src/spatie_parser.d.ts.map +1 -0
- package/dist/src/spatie_parser.js +134 -0
- package/dist/src/spatie_parser.js.map +1 -0
- package/dist/src/testing.d.ts +73 -0
- package/dist/src/testing.d.ts.map +1 -0
- package/dist/src/testing.js +136 -0
- package/dist/src/testing.js.map +1 -0
- package/dist/src/types.d.ts +226 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/validate-column-filter.d.ts +30 -0
- package/dist/src/validate-column-filter.d.ts.map +1 -0
- package/dist/src/validate-column-filter.js +148 -0
- package/dist/src/validate-column-filter.js.map +1 -0
- package/dist/src/value-shape.d.ts +18 -0
- package/dist/src/value-shape.d.ts.map +1 -0
- package/dist/src/value-shape.js +36 -0
- package/dist/src/value-shape.js.map +1 -0
- package/dist/stubs/main.d.ts +3 -0
- package/dist/stubs/main.d.ts.map +1 -0
- package/dist/stubs/main.js +4 -0
- package/dist/stubs/main.js.map +1 -0
- package/dist/stubs/make/filter/main.stub +45 -0
- package/package.json +102 -0
- package/skills/filter-basics/SKILL.md +285 -0
- package/skills/filter-codegen/SKILL.md +246 -0
- package/skills/filter-definitions/SKILL.md +340 -0
- package/skills/filter-querying/SKILL.md +319 -0
- package/skills/filter-safety/SKILL.md +304 -0
- package/stubs/main.ts +4 -0
- package/stubs/make/filter/main.stub +45 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { BaseCommand } from '@adonisjs/core/ace';
|
|
2
|
+
import type { CommandOptions } from '@adonisjs/core/types/ace';
|
|
3
|
+
/**
|
|
4
|
+
* `node ace make:filter-client [entrypoint]` — the idiomatic AdonisJS equivalent
|
|
5
|
+
* of the NestJS `nestjs-filter` codegen bin.
|
|
6
|
+
*
|
|
7
|
+
* Where NestJS ships a standalone `reflect-metadata` bin that reads decorator
|
|
8
|
+
* metadata off entity classes, the Adonis filter declaration is already a plain
|
|
9
|
+
* runtime {@link FilterClientManifest} (`defineFilter(...)` specs), so this is a
|
|
10
|
+
* thin IO wrapper: it loads the app's declared `filters` manifest, hands it to
|
|
11
|
+
* the pure {@link generateFilterClients} core, and writes each emitted typed
|
|
12
|
+
* client to disk. All generation logic lives in `../src/generate_client.ts` and
|
|
13
|
+
* is unit-tested there — this command only does the file IO.
|
|
14
|
+
*
|
|
15
|
+
* The entrypoint module (default `config/filter.js`) must export a `filters`
|
|
16
|
+
* manifest (named export, or default):
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* // config/filter.ts
|
|
20
|
+
* import { defineFilter } from '@adonis-agora/filter'
|
|
21
|
+
* export const filters = {
|
|
22
|
+
* people: { spec: defineFilter({ filterable: ['name', 'age'] }), fieldTypes: { age: { kind: 'number' } } },
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export default class MakeFilterClient extends BaseCommand {
|
|
27
|
+
#private;
|
|
28
|
+
static commandName: string;
|
|
29
|
+
static description: string;
|
|
30
|
+
static options: CommandOptions;
|
|
31
|
+
entrypoint: string;
|
|
32
|
+
output: string;
|
|
33
|
+
run(): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=make_filter_client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"make_filter_client.d.ts","sourceRoot":"","sources":["../../commands/make_filter_client.ts"],"names":[],"mappings":"AAEA,OAAO,EAAQ,WAAW,EAAS,MAAM,oBAAoB,CAAC;AAC9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAI/D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,OAAO,OAAO,gBAAiB,SAAQ,WAAW;;IACvD,OAAgB,WAAW,SAAwB;IACnD,OAAgB,WAAW,SAC4C;IACvE,OAAgB,OAAO,EAAE,cAAc,CAAsB;IAOrD,UAAU,EAAE,MAAM,CAAC;IAMnB,MAAM,EAAE,MAAM,CAAC;IAER,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAelC;CAuBF"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
8
|
+
import { isAbsolute, join } from 'node:path';
|
|
9
|
+
import { args, BaseCommand, flags } from '@adonisjs/core/ace';
|
|
10
|
+
import { generateFilterClients } from '../src/generate_client.js';
|
|
11
|
+
/**
|
|
12
|
+
* `node ace make:filter-client [entrypoint]` — the idiomatic AdonisJS equivalent
|
|
13
|
+
* of the NestJS `nestjs-filter` codegen bin.
|
|
14
|
+
*
|
|
15
|
+
* Where NestJS ships a standalone `reflect-metadata` bin that reads decorator
|
|
16
|
+
* metadata off entity classes, the Adonis filter declaration is already a plain
|
|
17
|
+
* runtime {@link FilterClientManifest} (`defineFilter(...)` specs), so this is a
|
|
18
|
+
* thin IO wrapper: it loads the app's declared `filters` manifest, hands it to
|
|
19
|
+
* the pure {@link generateFilterClients} core, and writes each emitted typed
|
|
20
|
+
* client to disk. All generation logic lives in `../src/generate_client.ts` and
|
|
21
|
+
* is unit-tested there — this command only does the file IO.
|
|
22
|
+
*
|
|
23
|
+
* The entrypoint module (default `config/filter.js`) must export a `filters`
|
|
24
|
+
* manifest (named export, or default):
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* // config/filter.ts
|
|
28
|
+
* import { defineFilter } from '@adonis-agora/filter'
|
|
29
|
+
* export const filters = {
|
|
30
|
+
* people: { spec: defineFilter({ filterable: ['name', 'age'] }), fieldTypes: { age: { kind: 'number' } } },
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export default class MakeFilterClient extends BaseCommand {
|
|
35
|
+
static commandName = 'make:filter-client';
|
|
36
|
+
static description = "Generate typed filter clients from the app's declared filter specs";
|
|
37
|
+
static options = { startApp: true };
|
|
38
|
+
async run() {
|
|
39
|
+
const manifest = await this.#loadManifest();
|
|
40
|
+
if (!manifest)
|
|
41
|
+
return;
|
|
42
|
+
const outDir = isAbsolute(this.output) ? this.output : this.app.makePath(this.output);
|
|
43
|
+
await mkdir(outDir, { recursive: true });
|
|
44
|
+
const generated = generateFilterClients(manifest);
|
|
45
|
+
for (const client of generated) {
|
|
46
|
+
const path = join(outDir, client.filename);
|
|
47
|
+
await writeFile(path, client.code, 'utf-8');
|
|
48
|
+
this.logger.action(`create ${path}`).succeeded();
|
|
49
|
+
}
|
|
50
|
+
this.logger.success(`Generated ${generated.length} filter client(s) into ${this.output}.`);
|
|
51
|
+
}
|
|
52
|
+
/** Import the entrypoint module and read its `filters` manifest (named or default export). */
|
|
53
|
+
async #loadManifest() {
|
|
54
|
+
const url = this.app.makeURL(this.entrypoint).href;
|
|
55
|
+
let mod;
|
|
56
|
+
try {
|
|
57
|
+
mod = (await import(url));
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
this.logger.error(`Could not import filter manifest "${this.entrypoint}": ${String(error)}`);
|
|
61
|
+
this.exitCode = 1;
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
const manifest = (mod.filters ?? mod.default);
|
|
65
|
+
if (!manifest || Object.keys(manifest).length === 0) {
|
|
66
|
+
this.logger.warning(`No \`filters\` manifest exported from "${this.entrypoint}" — nothing to generate.`);
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
return manifest;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
__decorate([
|
|
73
|
+
args.string({
|
|
74
|
+
description: 'Module exporting a `filters` manifest',
|
|
75
|
+
required: false,
|
|
76
|
+
default: 'config/filter.js',
|
|
77
|
+
})
|
|
78
|
+
], MakeFilterClient.prototype, "entrypoint", void 0);
|
|
79
|
+
__decorate([
|
|
80
|
+
flags.string({
|
|
81
|
+
description: 'Output directory for the generated clients',
|
|
82
|
+
default: 'app/generated/filters',
|
|
83
|
+
})
|
|
84
|
+
], MakeFilterClient.prototype, "output", void 0);
|
|
85
|
+
//# sourceMappingURL=make_filter_client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"make_filter_client.js","sourceRoot":"","sources":["../../commands/make_filter_client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type Configure from '@adonisjs/core/commands/configure';
|
|
2
|
+
/**
|
|
3
|
+
* `node ace add @adonis-agora/filter` (or `node ace configure @adonis-agora/filter`)
|
|
4
|
+
* — auto-wires the package into an AdonisJS app:
|
|
5
|
+
*
|
|
6
|
+
* 1. registers the service provider in `adonisrc.ts`, which installs the
|
|
7
|
+
* chainable `applyFilterFromRequest` / `filterPaginate` Lucid macros;
|
|
8
|
+
* 2. registers the ace commands barrel, which is what makes `make:filter-client`
|
|
9
|
+
* show up in `node ace list`.
|
|
10
|
+
*
|
|
11
|
+
* There is no config file to publish: a filter policy is a per-model
|
|
12
|
+
* `defineFilterSpec` call in your own code, not global configuration.
|
|
13
|
+
*/
|
|
14
|
+
export declare function configure(command: Configure): Promise<void>;
|
|
15
|
+
//# sourceMappingURL=configure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"configure.d.ts","sourceRoot":"","sources":["../configure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,SAAS,MAAM,mCAAmC,CAAC;AAE/D;;;;;;;;;;;GAWG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,SAAS,iBAOjD"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `node ace add @adonis-agora/filter` (or `node ace configure @adonis-agora/filter`)
|
|
3
|
+
* — auto-wires the package into an AdonisJS app:
|
|
4
|
+
*
|
|
5
|
+
* 1. registers the service provider in `adonisrc.ts`, which installs the
|
|
6
|
+
* chainable `applyFilterFromRequest` / `filterPaginate` Lucid macros;
|
|
7
|
+
* 2. registers the ace commands barrel, which is what makes `make:filter-client`
|
|
8
|
+
* show up in `node ace list`.
|
|
9
|
+
*
|
|
10
|
+
* There is no config file to publish: a filter policy is a per-model
|
|
11
|
+
* `defineFilterSpec` call in your own code, not global configuration.
|
|
12
|
+
*/
|
|
13
|
+
export async function configure(command) {
|
|
14
|
+
const codemods = await command.createCodemods();
|
|
15
|
+
await codemods.updateRcFile((rcFile) => {
|
|
16
|
+
rcFile.addProvider('@adonis-agora/filter/filter_provider');
|
|
17
|
+
rcFile.addCommand('@adonis-agora/filter/commands');
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=configure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"configure.js","sourceRoot":"","sources":["../configure.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,OAAkB;IAChD,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,cAAc,EAAE,CAAC;IAEhD,MAAM,QAAQ,CAAC,YAAY,CAAC,CAAC,MAAM,EAAE,EAAE;QACrC,MAAM,CAAC,WAAW,CAAC,sCAAsC,CAAC,CAAC;QAC3D,MAAM,CAAC,UAAU,CAAC,+BAA+B,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ApplicationService } from '@adonisjs/core/types';
|
|
2
|
+
/**
|
|
3
|
+
* Wires `@adonis-agora/filter`'s chainable Lucid macros into the application.
|
|
4
|
+
*
|
|
5
|
+
* On boot it registers the `applyFilterFromRequest` and `filterPaginate` macros
|
|
6
|
+
* onto Lucid's `ModelQueryBuilder`, so any model query gains the method-call form
|
|
7
|
+
* of the free `applyFilterFromRequest` helper:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* const rows = await User.query()
|
|
11
|
+
* .where('tenantId', tenant.id)
|
|
12
|
+
* .applyFilterFromRequest(userFilter, ctx)
|
|
13
|
+
* .orderBy('createdAt', 'desc')
|
|
14
|
+
*
|
|
15
|
+
* const page = await User.query().filterPaginate(userFilter, ctx)
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Register it in `adonisrc.ts` (`providers`). The free functions
|
|
19
|
+
* (`applyFilterFromRequest`, `applyCursorFromRequest`) work without it — this
|
|
20
|
+
* provider only adds the chainable sugar, so `@adonisjs/lucid` is loaded lazily
|
|
21
|
+
* here (not a hard dependency of the package).
|
|
22
|
+
*/
|
|
23
|
+
export default class FilterProvider {
|
|
24
|
+
protected app: ApplicationService;
|
|
25
|
+
constructor(app: ApplicationService);
|
|
26
|
+
boot(): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=filter_provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter_provider.d.ts","sourceRoot":"","sources":["../../providers/filter_provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAG/D;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,OAAO,OAAO,cAAc;IACrB,SAAS,CAAC,GAAG,EAAE,kBAAkB;IAA7C,YAAsB,GAAG,EAAE,kBAAkB,EAAI;IAE3C,IAAI,kBAGT;CACF"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { registerFilterMacros } from '../src/lucid_macros.js';
|
|
2
|
+
/**
|
|
3
|
+
* Wires `@adonis-agora/filter`'s chainable Lucid macros into the application.
|
|
4
|
+
*
|
|
5
|
+
* On boot it registers the `applyFilterFromRequest` and `filterPaginate` macros
|
|
6
|
+
* onto Lucid's `ModelQueryBuilder`, so any model query gains the method-call form
|
|
7
|
+
* of the free `applyFilterFromRequest` helper:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* const rows = await User.query()
|
|
11
|
+
* .where('tenantId', tenant.id)
|
|
12
|
+
* .applyFilterFromRequest(userFilter, ctx)
|
|
13
|
+
* .orderBy('createdAt', 'desc')
|
|
14
|
+
*
|
|
15
|
+
* const page = await User.query().filterPaginate(userFilter, ctx)
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Register it in `adonisrc.ts` (`providers`). The free functions
|
|
19
|
+
* (`applyFilterFromRequest`, `applyCursorFromRequest`) work without it — this
|
|
20
|
+
* provider only adds the chainable sugar, so `@adonisjs/lucid` is loaded lazily
|
|
21
|
+
* here (not a hard dependency of the package).
|
|
22
|
+
*/
|
|
23
|
+
export default class FilterProvider {
|
|
24
|
+
app;
|
|
25
|
+
constructor(app) {
|
|
26
|
+
this.app = app;
|
|
27
|
+
}
|
|
28
|
+
async boot() {
|
|
29
|
+
const { ModelQueryBuilder } = await import('@adonisjs/lucid/orm');
|
|
30
|
+
registerFilterMacros(ModelQueryBuilder);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=filter_provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter_provider.js","sourceRoot":"","sources":["../../providers/filter_provider.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAE9D;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,OAAO,OAAO,cAAc;IACX,GAAG;IAAzB,YAAsB,GAAuB;mBAAvB,GAAG;IAAuB,CAAC;IAEjD,KAAK,CAAC,IAAI;QACR,MAAM,EAAE,iBAAiB,EAAE,GAAG,MAAM,MAAM,CAAC,qBAAqB,CAAC,CAAC;QAClE,oBAAoB,CAAC,iBAAiB,CAAC,CAAC;IAC1C,CAAC;CACF"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { RelationSpec } from './filter_spec.js';
|
|
2
|
+
import type { ColumnFilter } from './operators.js';
|
|
3
|
+
import type { ComputedFields, ComputedSource } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* The structural slice of a booted Lucid relation the aggregate-subquery
|
|
6
|
+
* compiler reads. A real `@adonisjs/lucid` `HasMany` / `ManyToMany` relation
|
|
7
|
+
* satisfies it — declared locally so this module never hard-imports Lucid. The
|
|
8
|
+
* key-column fields are optional because they differ by relation kind
|
|
9
|
+
* (one-to-many exposes `foreignKeyColumnName`; many-to-many exposes the pivot
|
|
10
|
+
* fields), and only the relevant ones are read per kind.
|
|
11
|
+
*/
|
|
12
|
+
export interface LucidRelationLike {
|
|
13
|
+
/** Relation kind — `'hasMany'` / `'manyToMany'` are the to-many kinds we correlate. */
|
|
14
|
+
type: string;
|
|
15
|
+
/** Compute the relation's key columns from the naming strategy (idempotent). */
|
|
16
|
+
boot(): void;
|
|
17
|
+
/** The related (child) model — its `table` is the child table name. */
|
|
18
|
+
relatedModel(): {
|
|
19
|
+
table?: string;
|
|
20
|
+
};
|
|
21
|
+
foreignKeyColumnName?: string;
|
|
22
|
+
localKeyColumnName?: string;
|
|
23
|
+
relatedKeyColumnName?: string;
|
|
24
|
+
pivotTable?: string;
|
|
25
|
+
pivotForeignKey?: string;
|
|
26
|
+
pivotRelatedForeignKey?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The structural slice of a Lucid model the aggregate discovery reads: its table
|
|
30
|
+
* name and a relation accessor. `$getRelation` is typed with `any` for the same
|
|
31
|
+
* reason `QueryBuilderLike.whereHas` is — Lucid types it with a generic
|
|
32
|
+
* literal-union `Name`, and `string` is not assignable to that union, so a
|
|
33
|
+
* narrower signature would make every real model fail to satisfy this interface.
|
|
34
|
+
*/
|
|
35
|
+
export interface LucidModelLike {
|
|
36
|
+
table?: string;
|
|
37
|
+
$getRelation(name: any): LucidRelationLike | undefined;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The `EXISTS` / `NOT EXISTS` predicate equivalent to a filter on a discovered
|
|
41
|
+
* `$count` field, when the filter only asks whether the collection is empty:
|
|
42
|
+
* `gt 0`, `gte 1` and `notEquals 0` mean non-empty; `equals 0`, `lte 0` and
|
|
43
|
+
* `lt 1` mean empty. `EXISTS` stops at the first related row where the counting
|
|
44
|
+
* subquery visits all of them. `undefined` for any other filter or source.
|
|
45
|
+
*/
|
|
46
|
+
export declare function countExistencePredicate(source: ComputedSource, alias: string, filter: ColumnFilter): string | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Discover to-many aggregate {@link ComputedSource}s from a Lucid model's
|
|
49
|
+
* relation metadata, for the relations the spec whitelisted. For each declared
|
|
50
|
+
* to-many relation (hasMany / manyToMany) this synthesises:
|
|
51
|
+
*
|
|
52
|
+
* - `<rel>.$count` — always;
|
|
53
|
+
* - `<rel>.$sum|$avg|$min|$max.<col>` — one per column the relation's
|
|
54
|
+
* {@link RelationSpec.aggregates} declares (Lucid does not reflect SQL column
|
|
55
|
+
* types, so the dev asserts numeric-ness by listing the column there).
|
|
56
|
+
*
|
|
57
|
+
* To-one relations (belongsTo / hasOne) are excluded. The whole thing degrades
|
|
58
|
+
* gracefully: a relation that is not on the model, is to-one, or cannot be
|
|
59
|
+
* booted (e.g. no DB adapter yet) is simply skipped, and a model without the
|
|
60
|
+
* introspection capability yields an empty map — so aggregates activate only
|
|
61
|
+
* when the metadata is actually available.
|
|
62
|
+
*
|
|
63
|
+
* The result is merged into the spec's `computed` map, so aggregate fields ARE
|
|
64
|
+
* computed fields with auto-generated correlated-subquery sources — they reuse
|
|
65
|
+
* the exact same runner routing, allow-list bypass (their discovery IS their
|
|
66
|
+
* allow-list, gated by the relation whitelist), injection-safe value binding,
|
|
67
|
+
* and codegen surfacing.
|
|
68
|
+
*/
|
|
69
|
+
export declare function discoverAggregateSources(model: LucidModelLike | undefined, relations: Readonly<Record<string, RelationSpec>>): ComputedFields;
|
|
70
|
+
//# sourceMappingURL=aggregate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aggregate.d.ts","sourceRoot":"","sources":["../../src/aggregate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,EAAmB,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAGlF;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC,uFAAuF;IACvF,IAAI,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,IAAI,IAAI,IAAI,CAAC;IACb,uEAAuE;IACvE,YAAY,IAAI;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAEnC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAE5B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,YAAY,CAAC,IAAI,EAAE,GAAG,GAAG,iBAAiB,GAAG,SAAS,CAAC;CACxD;AAsBD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,YAAY,GACnB,MAAM,GAAG,SAAS,CA6BpB;AAqFD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,cAAc,GAAG,SAAS,EACjC,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,GAChD,cAAc,CA8DhB"}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { normalizeOperator } from './validate-column-filter.js';
|
|
2
|
+
/** Column functions (need a numeric child column); `count` needs none. */
|
|
3
|
+
const COLUMN_FNS = ['sum', 'avg', 'min', 'max'];
|
|
4
|
+
/** Double-quote one identifier segment for Postgres, escaping embedded quotes. */
|
|
5
|
+
function q(ident) {
|
|
6
|
+
return `"${ident.replace(/"/g, '""')}"`;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The correlated `FROM … WHERE <correlation>` clause behind each discovered
|
|
10
|
+
* `$count` source. Keyed by the source function, so a hand-written computed
|
|
11
|
+
* field never has one.
|
|
12
|
+
*/
|
|
13
|
+
const countRowSets = new WeakMap();
|
|
14
|
+
/** Is `value` the integer `n`, as a number or as the string a query parameter carries? */
|
|
15
|
+
function isInteger(value, n) {
|
|
16
|
+
return value === n || value === String(n);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The `EXISTS` / `NOT EXISTS` predicate equivalent to a filter on a discovered
|
|
20
|
+
* `$count` field, when the filter only asks whether the collection is empty:
|
|
21
|
+
* `gt 0`, `gte 1` and `notEquals 0` mean non-empty; `equals 0`, `lte 0` and
|
|
22
|
+
* `lt 1` mean empty. `EXISTS` stops at the first related row where the counting
|
|
23
|
+
* subquery visits all of them. `undefined` for any other filter or source.
|
|
24
|
+
*/
|
|
25
|
+
export function countExistencePredicate(source, alias, filter) {
|
|
26
|
+
if (typeof source !== 'function')
|
|
27
|
+
return undefined;
|
|
28
|
+
const rowSet = countRowSets.get(source);
|
|
29
|
+
if (!rowSet)
|
|
30
|
+
return undefined;
|
|
31
|
+
const value = filter.value;
|
|
32
|
+
let exists;
|
|
33
|
+
switch (normalizeOperator(filter.operator)) {
|
|
34
|
+
case 'gt':
|
|
35
|
+
case 'notEquals':
|
|
36
|
+
if (!isInteger(value, 0))
|
|
37
|
+
return undefined;
|
|
38
|
+
exists = true;
|
|
39
|
+
break;
|
|
40
|
+
case 'gte':
|
|
41
|
+
if (!isInteger(value, 1))
|
|
42
|
+
return undefined;
|
|
43
|
+
exists = true;
|
|
44
|
+
break;
|
|
45
|
+
case 'equals':
|
|
46
|
+
case 'lte':
|
|
47
|
+
if (!isInteger(value, 0))
|
|
48
|
+
return undefined;
|
|
49
|
+
exists = false;
|
|
50
|
+
break;
|
|
51
|
+
case 'lt':
|
|
52
|
+
if (!isInteger(value, 1))
|
|
53
|
+
return undefined;
|
|
54
|
+
exists = false;
|
|
55
|
+
break;
|
|
56
|
+
default:
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
return `${exists ? 'EXISTS' : 'NOT EXISTS'} (SELECT 1 ${rowSet({ alias })})`;
|
|
60
|
+
}
|
|
61
|
+
/** A `$count` source over the rows `rowSet` (`FROM … WHERE …`) selects. */
|
|
62
|
+
function countSource(rowSet) {
|
|
63
|
+
const source = (ctx) => `(SELECT COUNT(*) ${rowSet(ctx)})`;
|
|
64
|
+
countRowSets.set(source, rowSet);
|
|
65
|
+
return source;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The scalar aggregate expression over an (already-quoted) child column. Empty
|
|
69
|
+
* collection semantics: `sum` coalesces to 0; `avg`/`min`/`max` stay NULL (their
|
|
70
|
+
* natural result over zero rows); `count` is naturally 0.
|
|
71
|
+
*/
|
|
72
|
+
function aggregateExpr(fn, quotedColumn) {
|
|
73
|
+
switch (fn) {
|
|
74
|
+
case 'count':
|
|
75
|
+
return 'COUNT(*)';
|
|
76
|
+
case 'sum':
|
|
77
|
+
return `COALESCE(SUM(${quotedColumn}),0)`;
|
|
78
|
+
case 'avg':
|
|
79
|
+
return `AVG(${quotedColumn})`;
|
|
80
|
+
case 'min':
|
|
81
|
+
return `MIN(${quotedColumn})`;
|
|
82
|
+
case 'max':
|
|
83
|
+
return `MAX(${quotedColumn})`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* ONE_TO_MANY correlation: a direct FK on the child row points back at the
|
|
88
|
+
* root's PK. `column` omitted → `$count`.
|
|
89
|
+
*
|
|
90
|
+
* (SELECT <agg> FROM "child" WHERE "child"."fk" = "<alias>"."pk")
|
|
91
|
+
*/
|
|
92
|
+
function oneToManySource(fn, childTable, fk, pk, column) {
|
|
93
|
+
if (fn === 'count') {
|
|
94
|
+
return countSource(({ alias }) => `FROM ${q(childTable)} WHERE ${q(childTable)}.${q(fk)} = ${q(alias)}.${q(pk)}`);
|
|
95
|
+
}
|
|
96
|
+
const agg = aggregateExpr(fn, column ? `${q(childTable)}.${q(column)}` : '');
|
|
97
|
+
return ({ alias }) => `(SELECT ${agg} FROM ${q(childTable)} WHERE ${q(childTable)}.${q(fk)} = ${q(alias)}.${q(pk)})`;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* MANY_TO_MANY correlation, mediated by a pivot table. `$count` counts pivot
|
|
101
|
+
* rows directly; the column functions JOIN pivot → child INSIDE the scalar
|
|
102
|
+
* subquery to reach the child column.
|
|
103
|
+
*
|
|
104
|
+
* $count: (SELECT COUNT(*) FROM "pivot" WHERE "pivot"."ownerFk" = "<alias>"."ownerPk")
|
|
105
|
+
* $agg: (SELECT <agg> FROM "child"
|
|
106
|
+
* JOIN "pivot" ON "child"."childPk" = "pivot"."inverseFk"
|
|
107
|
+
* WHERE "pivot"."ownerFk" = "<alias>"."ownerPk")
|
|
108
|
+
*/
|
|
109
|
+
function manyToManySource(fn, meta, column) {
|
|
110
|
+
if (fn === 'count') {
|
|
111
|
+
return countSource(({ alias }) => `FROM ${q(meta.pivot)} WHERE ${q(meta.pivot)}.${q(meta.ownerFk)} = ${q(alias)}.${q(meta.ownerPk)}`);
|
|
112
|
+
}
|
|
113
|
+
const agg = aggregateExpr(fn, column ? `${q(meta.childTable)}.${q(column)}` : '');
|
|
114
|
+
return ({ alias }) => `(SELECT ${agg} FROM ${q(meta.childTable)} JOIN ${q(meta.pivot)} ON ${q(meta.childTable)}.${q(meta.childPk)} = ${q(meta.pivot)}.${q(meta.inverseFk)} WHERE ${q(meta.pivot)}.${q(meta.ownerFk)} = ${q(alias)}.${q(meta.ownerPk)})`;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Discover to-many aggregate {@link ComputedSource}s from a Lucid model's
|
|
118
|
+
* relation metadata, for the relations the spec whitelisted. For each declared
|
|
119
|
+
* to-many relation (hasMany / manyToMany) this synthesises:
|
|
120
|
+
*
|
|
121
|
+
* - `<rel>.$count` — always;
|
|
122
|
+
* - `<rel>.$sum|$avg|$min|$max.<col>` — one per column the relation's
|
|
123
|
+
* {@link RelationSpec.aggregates} declares (Lucid does not reflect SQL column
|
|
124
|
+
* types, so the dev asserts numeric-ness by listing the column there).
|
|
125
|
+
*
|
|
126
|
+
* To-one relations (belongsTo / hasOne) are excluded. The whole thing degrades
|
|
127
|
+
* gracefully: a relation that is not on the model, is to-one, or cannot be
|
|
128
|
+
* booted (e.g. no DB adapter yet) is simply skipped, and a model without the
|
|
129
|
+
* introspection capability yields an empty map — so aggregates activate only
|
|
130
|
+
* when the metadata is actually available.
|
|
131
|
+
*
|
|
132
|
+
* The result is merged into the spec's `computed` map, so aggregate fields ARE
|
|
133
|
+
* computed fields with auto-generated correlated-subquery sources — they reuse
|
|
134
|
+
* the exact same runner routing, allow-list bypass (their discovery IS their
|
|
135
|
+
* allow-list, gated by the relation whitelist), injection-safe value binding,
|
|
136
|
+
* and codegen surfacing.
|
|
137
|
+
*/
|
|
138
|
+
export function discoverAggregateSources(model, relations) {
|
|
139
|
+
if (!model || typeof model.$getRelation !== 'function')
|
|
140
|
+
return {};
|
|
141
|
+
const out = {};
|
|
142
|
+
for (const [relName, relSpec] of Object.entries(relations)) {
|
|
143
|
+
let rel;
|
|
144
|
+
try {
|
|
145
|
+
rel = model.$getRelation(relName);
|
|
146
|
+
if (!rel)
|
|
147
|
+
continue;
|
|
148
|
+
rel.boot();
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
continue; // Un-bootable relation (no adapter yet, etc.) → skip its aggregates.
|
|
152
|
+
}
|
|
153
|
+
const columns = relSpec.aggregates ?? [];
|
|
154
|
+
const childTable = rel.relatedModel?.().table;
|
|
155
|
+
if (rel.type === 'hasMany') {
|
|
156
|
+
const fk = rel.foreignKeyColumnName;
|
|
157
|
+
const pk = rel.localKeyColumnName;
|
|
158
|
+
if (!childTable || !fk || !pk)
|
|
159
|
+
continue;
|
|
160
|
+
out[`${relName}.$count`] = oneToManySource('count', childTable, fk, pk, undefined);
|
|
161
|
+
for (const col of columns) {
|
|
162
|
+
for (const fn of COLUMN_FNS) {
|
|
163
|
+
out[`${relName}.$${fn}.${col}`] = oneToManySource(fn, childTable, fk, pk, col);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else if (rel.type === 'manyToMany') {
|
|
168
|
+
const pivot = rel.pivotTable;
|
|
169
|
+
const ownerFk = rel.pivotForeignKey;
|
|
170
|
+
const inverseFk = rel.pivotRelatedForeignKey;
|
|
171
|
+
const ownerPk = rel.localKeyColumnName;
|
|
172
|
+
const childPk = rel.relatedKeyColumnName;
|
|
173
|
+
if (!pivot || !ownerFk || !ownerPk)
|
|
174
|
+
continue;
|
|
175
|
+
out[`${relName}.$count`] = manyToManySource('count', {
|
|
176
|
+
childTable: childTable ?? '',
|
|
177
|
+
childPk: childPk ?? '',
|
|
178
|
+
pivot,
|
|
179
|
+
ownerFk,
|
|
180
|
+
inverseFk: inverseFk ?? '',
|
|
181
|
+
ownerPk,
|
|
182
|
+
}, undefined);
|
|
183
|
+
if (childTable && inverseFk && childPk) {
|
|
184
|
+
for (const col of columns) {
|
|
185
|
+
for (const fn of COLUMN_FNS) {
|
|
186
|
+
out[`${relName}.$${fn}.${col}`] = manyToManySource(fn, { childTable, childPk, pivot, ownerFk, inverseFk, ownerPk }, col);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// to-one (belongsTo / hasOne) and any other kind → no aggregate keys.
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=aggregate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aggregate.js","sourceRoot":"","sources":["../../src/aggregate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, ORM-agnostic parser for to-many aggregate field paths — a verbatim port
|
|
3
|
+
* of the NestJS `nestjs-filter` `aggregate/aggregate-path.ts` (commit 3f21e1f).
|
|
4
|
+
* It only parses the client-facing field *string* into a structured
|
|
5
|
+
* {@link AggregatePath}; turning that into SQL (a correlated subquery) is the
|
|
6
|
+
* Lucid-specific compiler's job (see `filter_spec.ts` aggregate discovery).
|
|
7
|
+
*/
|
|
8
|
+
export type AggregateFn = 'count' | 'sum' | 'avg' | 'min' | 'max';
|
|
9
|
+
export interface AggregatePath {
|
|
10
|
+
relation: string;
|
|
11
|
+
fn: AggregateFn;
|
|
12
|
+
column?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Parses a to-many aggregate field path.
|
|
16
|
+
*
|
|
17
|
+
* Grammar (single relation hop only):
|
|
18
|
+
* - `<relation>.$count` → { relation, fn: 'count' }
|
|
19
|
+
* - `<relation>.$<fn>.<childColumn>` → { relation, fn, column } for fn in sum|avg|min|max
|
|
20
|
+
*
|
|
21
|
+
* Returns `null` for anything else so callers can fall through to
|
|
22
|
+
* non-aggregate field handling instead of throwing.
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseAggregatePath(path: string): AggregatePath | null;
|
|
25
|
+
//# sourceMappingURL=aggregate_path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aggregate_path.d.ts","sourceRoot":"","sources":["../../src/aggregate_path.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;AAClE,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,WAAW,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAID;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAWrE"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, ORM-agnostic parser for to-many aggregate field paths — a verbatim port
|
|
3
|
+
* of the NestJS `nestjs-filter` `aggregate/aggregate-path.ts` (commit 3f21e1f).
|
|
4
|
+
* It only parses the client-facing field *string* into a structured
|
|
5
|
+
* {@link AggregatePath}; turning that into SQL (a correlated subquery) is the
|
|
6
|
+
* Lucid-specific compiler's job (see `filter_spec.ts` aggregate discovery).
|
|
7
|
+
*/
|
|
8
|
+
const COLUMN_FNS = new Set(['sum', 'avg', 'min', 'max']);
|
|
9
|
+
/**
|
|
10
|
+
* Parses a to-many aggregate field path.
|
|
11
|
+
*
|
|
12
|
+
* Grammar (single relation hop only):
|
|
13
|
+
* - `<relation>.$count` → { relation, fn: 'count' }
|
|
14
|
+
* - `<relation>.$<fn>.<childColumn>` → { relation, fn, column } for fn in sum|avg|min|max
|
|
15
|
+
*
|
|
16
|
+
* Returns `null` for anything else so callers can fall through to
|
|
17
|
+
* non-aggregate field handling instead of throwing.
|
|
18
|
+
*/
|
|
19
|
+
export function parseAggregatePath(path) {
|
|
20
|
+
const parts = path.split('.');
|
|
21
|
+
// <rel>.$<fn> → 2 parts, $count
|
|
22
|
+
// <rel>.$<fn>.<col> → 3 parts, column fns
|
|
23
|
+
if (parts.length < 2 || parts.length > 3)
|
|
24
|
+
return null;
|
|
25
|
+
const [relation, token, column] = parts;
|
|
26
|
+
if (!relation || !token?.startsWith('$'))
|
|
27
|
+
return null;
|
|
28
|
+
const fn = token.slice(1);
|
|
29
|
+
if (fn === 'count')
|
|
30
|
+
return parts.length === 2 ? { relation, fn } : null;
|
|
31
|
+
if (COLUMN_FNS.has(fn))
|
|
32
|
+
return parts.length === 3 && column ? { relation, fn, column } : null;
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=aggregate_path.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aggregate_path.js","sourceRoot":"","sources":["../../src/aggregate_path.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,MAAM,UAAU,GAAG,IAAI,GAAG,CAAc,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;AAEtE;;;;;;;;;GASG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC9B,2CAA2C;IAC3C,+CAA+C;IAC/C,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACtD,MAAM,CAAC,QAAQ,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,KAAK,CAAC;IACxC,IAAI,CAAC,QAAQ,IAAI,CAAC,KAAK,EAAE,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACtD,MAAM,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAgB,CAAC;IACzC,IAAI,EAAE,KAAK,OAAO;QAAE,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACxE,IAAI,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAAE,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IAC9F,OAAO,IAAI,CAAC;AACd,CAAC"}
|