@gettersethya/mira-client 0.1.0-alpha.10
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/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/src/client/auth.d.ts +73 -0
- package/dist/src/client/auth.d.ts.map +1 -0
- package/dist/src/client/auth.js +72 -0
- package/dist/src/client/auth.js.map +1 -0
- package/dist/src/client/collection.d.ts +239 -0
- package/dist/src/client/collection.d.ts.map +1 -0
- package/dist/src/client/collection.js +137 -0
- package/dist/src/client/collection.js.map +1 -0
- package/dist/src/client/errors.d.ts +46 -0
- package/dist/src/client/errors.d.ts.map +1 -0
- package/dist/src/client/errors.js +40 -0
- package/dist/src/client/errors.js.map +1 -0
- package/dist/src/client/factory.d.ts +78 -0
- package/dist/src/client/factory.d.ts.map +1 -0
- package/dist/src/client/factory.js +108 -0
- package/dist/src/client/factory.js.map +1 -0
- package/dist/src/client/file.d.ts +116 -0
- package/dist/src/client/file.d.ts.map +1 -0
- package/dist/src/client/file.js +99 -0
- package/dist/src/client/file.js.map +1 -0
- package/dist/src/client/handler.d.ts +92 -0
- package/dist/src/client/handler.d.ts.map +1 -0
- package/dist/src/client/handler.js +62 -0
- package/dist/src/client/handler.js.map +1 -0
- package/dist/src/client/index.d.ts +13 -0
- package/dist/src/client/index.d.ts.map +1 -0
- package/dist/src/client/index.js +7 -0
- package/dist/src/client/index.js.map +1 -0
- package/dist/src/client/telemetry.d.ts +50 -0
- package/dist/src/client/telemetry.d.ts.map +1 -0
- package/dist/src/client/telemetry.js +32 -0
- package/dist/src/client/telemetry.js.map +1 -0
- package/dist/src/client/types.d.ts +137 -0
- package/dist/src/client/types.d.ts.map +1 -0
- package/dist/src/client/types.js +2 -0
- package/dist/src/client/types.js.map +1 -0
- package/dist/src/collection/auth.d.ts +79 -0
- package/dist/src/collection/auth.d.ts.map +1 -0
- package/dist/src/collection/auth.js +50 -0
- package/dist/src/collection/auth.js.map +1 -0
- package/dist/src/collection/base.d.ts +64 -0
- package/dist/src/collection/base.d.ts.map +1 -0
- package/dist/src/collection/base.js +40 -0
- package/dist/src/collection/base.js.map +1 -0
- package/dist/src/collection/bytes.d.ts +51 -0
- package/dist/src/collection/bytes.d.ts.map +1 -0
- package/dist/src/collection/bytes.js +51 -0
- package/dist/src/collection/bytes.js.map +1 -0
- package/dist/src/collection/errors.d.ts +29 -0
- package/dist/src/collection/errors.d.ts.map +1 -0
- package/dist/src/collection/errors.js +23 -0
- package/dist/src/collection/errors.js.map +1 -0
- package/dist/src/collection/field.d.ts +300 -0
- package/dist/src/collection/field.d.ts.map +1 -0
- package/dist/src/collection/field.js +175 -0
- package/dist/src/collection/field.js.map +1 -0
- package/dist/src/collection/index-builder.d.ts +59 -0
- package/dist/src/collection/index-builder.d.ts.map +1 -0
- package/dist/src/collection/index-builder.js +43 -0
- package/dist/src/collection/index-builder.js.map +1 -0
- package/dist/src/collection/index.d.ts +22 -0
- package/dist/src/collection/index.d.ts.map +1 -0
- package/dist/src/collection/index.js +21 -0
- package/dist/src/collection/index.js.map +1 -0
- package/dist/src/collection/serialize.d.ts +24 -0
- package/dist/src/collection/serialize.d.ts.map +1 -0
- package/dist/src/collection/serialize.js +127 -0
- package/dist/src/collection/serialize.js.map +1 -0
- package/dist/src/collection/types.d.ts +119 -0
- package/dist/src/collection/types.d.ts.map +1 -0
- package/dist/src/collection/types.js +2 -0
- package/dist/src/collection/types.js.map +1 -0
- package/dist/src/collection/view.d.ts +57 -0
- package/dist/src/collection/view.d.ts.map +1 -0
- package/dist/src/collection/view.js +49 -0
- package/dist/src/collection/view.js.map +1 -0
- package/dist/src/filter/builder.d.ts +127 -0
- package/dist/src/filter/builder.d.ts.map +1 -0
- package/dist/src/filter/builder.js +72 -0
- package/dist/src/filter/builder.js.map +1 -0
- package/dist/src/filter/compiler.d.ts +48 -0
- package/dist/src/filter/compiler.d.ts.map +1 -0
- package/dist/src/filter/compiler.js +132 -0
- package/dist/src/filter/compiler.js.map +1 -0
- package/dist/src/filter/index.d.ts +6 -0
- package/dist/src/filter/index.d.ts.map +1 -0
- package/dist/src/filter/index.js +4 -0
- package/dist/src/filter/index.js.map +1 -0
- package/dist/src/filter/types.d.ts +96 -0
- package/dist/src/filter/types.d.ts.map +1 -0
- package/dist/src/filter/types.js +44 -0
- package/dist/src/filter/types.js.map +1 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/rule/builder.d.ts +97 -0
- package/dist/src/rule/builder.d.ts.map +1 -0
- package/dist/src/rule/builder.js +126 -0
- package/dist/src/rule/builder.js.map +1 -0
- package/dist/src/rule/index.d.ts +3 -0
- package/dist/src/rule/index.d.ts.map +1 -0
- package/dist/src/rule/index.js +3 -0
- package/dist/src/rule/index.js.map +1 -0
- package/dist/src/rule/types.d.ts +129 -0
- package/dist/src/rule/types.d.ts.map +1 -0
- package/dist/src/rule/types.js +2 -0
- package/dist/src/rule/types.js.map +1 -0
- package/dist/tests/client/auth.test.d.ts +2 -0
- package/dist/tests/client/auth.test.d.ts.map +1 -0
- package/dist/tests/client/auth.test.js +67 -0
- package/dist/tests/client/auth.test.js.map +1 -0
- package/dist/tests/client/collection.test.d.ts +2 -0
- package/dist/tests/client/collection.test.d.ts.map +1 -0
- package/dist/tests/client/collection.test.js +593 -0
- package/dist/tests/client/collection.test.js.map +1 -0
- package/dist/tests/client/file.test.d.ts +2 -0
- package/dist/tests/client/file.test.d.ts.map +1 -0
- package/dist/tests/client/file.test.js +21 -0
- package/dist/tests/client/file.test.js.map +1 -0
- package/dist/tests/client/handler.test.d.ts +2 -0
- package/dist/tests/client/handler.test.d.ts.map +1 -0
- package/dist/tests/client/handler.test.js +24 -0
- package/dist/tests/client/handler.test.js.map +1 -0
- package/dist/tests/client/types.test.d.ts +2 -0
- package/dist/tests/client/types.test.d.ts.map +1 -0
- package/dist/tests/client/types.test.js +168 -0
- package/dist/tests/client/types.test.js.map +1 -0
- package/dist/tests/collection/auth.test.d.ts +2 -0
- package/dist/tests/collection/auth.test.d.ts.map +1 -0
- package/dist/tests/collection/auth.test.js +44 -0
- package/dist/tests/collection/auth.test.js.map +1 -0
- package/dist/tests/collection/base.test.d.ts +2 -0
- package/dist/tests/collection/base.test.d.ts.map +1 -0
- package/dist/tests/collection/base.test.js +70 -0
- package/dist/tests/collection/base.test.js.map +1 -0
- package/dist/tests/collection/bytes.test.d.ts +2 -0
- package/dist/tests/collection/bytes.test.d.ts.map +1 -0
- package/dist/tests/collection/bytes.test.js +9 -0
- package/dist/tests/collection/bytes.test.js.map +1 -0
- package/dist/tests/collection/field.test.d.ts +2 -0
- package/dist/tests/collection/field.test.d.ts.map +1 -0
- package/dist/tests/collection/field.test.js +140 -0
- package/dist/tests/collection/field.test.js.map +1 -0
- package/dist/tests/collection/index-type.test.d.ts +2 -0
- package/dist/tests/collection/index-type.test.d.ts.map +1 -0
- package/dist/tests/collection/index-type.test.js +28 -0
- package/dist/tests/collection/index-type.test.js.map +1 -0
- package/dist/tests/collection/serialize.test.d.ts +2 -0
- package/dist/tests/collection/serialize.test.d.ts.map +1 -0
- package/dist/tests/collection/serialize.test.js +165 -0
- package/dist/tests/collection/serialize.test.js.map +1 -0
- package/dist/tests/collection/view.test.d.ts +2 -0
- package/dist/tests/collection/view.test.d.ts.map +1 -0
- package/dist/tests/collection/view.test.js +66 -0
- package/dist/tests/collection/view.test.js.map +1 -0
- package/dist/tests/filter/compiler.test.d.ts +2 -0
- package/dist/tests/filter/compiler.test.d.ts.map +1 -0
- package/dist/tests/filter/compiler.test.js +156 -0
- package/dist/tests/filter/compiler.test.js.map +1 -0
- package/dist/tests/rule/builder.test.d.ts +2 -0
- package/dist/tests/rule/builder.test.d.ts.map +1 -0
- package/dist/tests/rule/builder.test.js +290 -0
- package/dist/tests/rule/builder.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +39 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { RuleBuilder } from "../rule/builder.js";
|
|
2
|
+
import type { RuleMap } from "../rule/types.js";
|
|
3
|
+
import type { CollectionSchema, FieldDef, FieldsMap } from "./types.js";
|
|
4
|
+
type ViewFields = {
|
|
5
|
+
id: FieldDef & {
|
|
6
|
+
viewOnly: true;
|
|
7
|
+
};
|
|
8
|
+
seqId: FieldDef & {
|
|
9
|
+
viewOnly: true;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
type RuleCb<F extends FieldsMap> = (R: RuleBuilder<F>) => RuleMap;
|
|
13
|
+
/**
|
|
14
|
+
* Builder returned by `ViewCollection.define()`.
|
|
15
|
+
* Immediately satisfies `AnyCollectionDef` — no terminal `.build()` needed.
|
|
16
|
+
* Chain `.rules()` to add access rules. Views have no `.indexes()` — they are not physical tables.
|
|
17
|
+
* Each chained method returns a new builder; the original is unchanged.
|
|
18
|
+
*/
|
|
19
|
+
export type ViewCollectionBuilder<F extends FieldsMap> = {
|
|
20
|
+
name: string;
|
|
21
|
+
fields: F;
|
|
22
|
+
schema: CollectionSchema;
|
|
23
|
+
/**
|
|
24
|
+
* Add per-action access rules. No rule means deny all.
|
|
25
|
+
* Pass a callback to get a typed rule builder with auto-completion over field names.
|
|
26
|
+
*/
|
|
27
|
+
rules(cb: RuleCb<F>): ViewCollectionBuilder<F>;
|
|
28
|
+
};
|
|
29
|
+
/** @internal Exported for testing only. */
|
|
30
|
+
export declare function validateViewFields(name: string, fields: Record<string, {
|
|
31
|
+
viewOnly?: boolean;
|
|
32
|
+
}>): void;
|
|
33
|
+
/**
|
|
34
|
+
* Defines a read-only view collection backed by a raw SQL query.
|
|
35
|
+
* Requires `id` and `seqId` in the field map, both declared with `.view()`.
|
|
36
|
+
* No indexes are supported.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* const ActivePosts = ViewCollection.define(
|
|
40
|
+
* "active_posts",
|
|
41
|
+
* `WITH ranked AS (
|
|
42
|
+
* SELECT p.id, p.title, ROW_NUMBER() OVER (ORDER BY p.created DESC) AS seqId
|
|
43
|
+
* FROM posts p WHERE p.status = 'active'
|
|
44
|
+
* ) SELECT * FROM ranked`,
|
|
45
|
+
* {
|
|
46
|
+
* id: Field.text().view(),
|
|
47
|
+
* seqId: Field.integer().view(),
|
|
48
|
+
* title: Field.text().view()
|
|
49
|
+
* }
|
|
50
|
+
* )
|
|
51
|
+
* .rules((R) => ({ list: R.public(), view: R.public() }))
|
|
52
|
+
*/
|
|
53
|
+
export declare const ViewCollection: {
|
|
54
|
+
define<F extends ViewFields & FieldsMap>(name: string, query: string, fields: F): ViewCollectionBuilder<F>;
|
|
55
|
+
};
|
|
56
|
+
export {};
|
|
57
|
+
//# sourceMappingURL=view.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"view.d.ts","sourceRoot":"","sources":["../../../src/collection/view.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAE9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAEvE,KAAK,UAAU,GAAG;IAChB,EAAE,EAAK,QAAQ,GAAG;QAAE,QAAQ,EAAE,IAAI,CAAA;KAAE,CAAA;IACpC,KAAK,EAAE,QAAQ,GAAG;QAAE,QAAQ,EAAE,IAAI,CAAA;KAAE,CAAA;CACrC,CAAA;AAED,KAAK,MAAM,CAAC,CAAC,SAAS,SAAS,IAAI,CACjC,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,KACd,OAAO,CAAA;AAEZ;;;;;GAKG;AACH,MAAM,MAAM,qBAAqB,CAAC,CAAC,SAAS,SAAS,IAAI;IACvD,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,CAAC,CAAA;IACT,MAAM,EAAE,gBAAgB,CAAA;IACxB;;;OAGG;IACH,KAAK,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,qBAAqB,CAAC,CAAC,CAAC,CAAA;CAC/C,CAAA;AAED,2CAA2C;AAC3C,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,GAAG,IAAI,CAKrG;AAqBD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,cAAc;WAClB,CAAC,SAAS,UAAU,GAAG,SAAS,QAC/B,MAAM,SACL,MAAM,UACL,CAAC,GACR,qBAAqB,CAAC,CAAC,CAAC;CAI5B,CAAA"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { makeRuleBuilder } from "../rule/builder.js";
|
|
2
|
+
import { toJSONSchema } from "./serialize.js";
|
|
3
|
+
/** @internal Exported for testing only. */
|
|
4
|
+
export function validateViewFields(name, fields) {
|
|
5
|
+
if (!fields.id || fields.id.viewOnly !== true)
|
|
6
|
+
throw new Error(`ViewCollection "${name}": "id" must be declared with .view()`);
|
|
7
|
+
if (!fields.seqId || fields.seqId.viewOnly !== true)
|
|
8
|
+
throw new Error(`ViewCollection "${name}": "seqId" must be declared with .view()`);
|
|
9
|
+
}
|
|
10
|
+
function makeViewBuilder(name, query, fields, rulesCb) {
|
|
11
|
+
const rules = rulesCb?.(makeRuleBuilder());
|
|
12
|
+
const schema = toJSONSchema("view", name, fields, {
|
|
13
|
+
viewQuery: query,
|
|
14
|
+
...(rules !== undefined ? { rules } : {})
|
|
15
|
+
});
|
|
16
|
+
return {
|
|
17
|
+
name,
|
|
18
|
+
fields,
|
|
19
|
+
schema,
|
|
20
|
+
rules: (cb) => makeViewBuilder(name, query, fields, cb)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Defines a read-only view collection backed by a raw SQL query.
|
|
25
|
+
* Requires `id` and `seqId` in the field map, both declared with `.view()`.
|
|
26
|
+
* No indexes are supported.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* const ActivePosts = ViewCollection.define(
|
|
30
|
+
* "active_posts",
|
|
31
|
+
* `WITH ranked AS (
|
|
32
|
+
* SELECT p.id, p.title, ROW_NUMBER() OVER (ORDER BY p.created DESC) AS seqId
|
|
33
|
+
* FROM posts p WHERE p.status = 'active'
|
|
34
|
+
* ) SELECT * FROM ranked`,
|
|
35
|
+
* {
|
|
36
|
+
* id: Field.text().view(),
|
|
37
|
+
* seqId: Field.integer().view(),
|
|
38
|
+
* title: Field.text().view()
|
|
39
|
+
* }
|
|
40
|
+
* )
|
|
41
|
+
* .rules((R) => ({ list: R.public(), view: R.public() }))
|
|
42
|
+
*/
|
|
43
|
+
export const ViewCollection = {
|
|
44
|
+
define(name, query, fields) {
|
|
45
|
+
validateViewFields(name, fields);
|
|
46
|
+
return makeViewBuilder(name, query, fields);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
//# sourceMappingURL=view.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"view.js","sourceRoot":"","sources":["../../../src/collection/view.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAGnD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AA6B7C,2CAA2C;AAC3C,MAAM,UAAU,kBAAkB,CAAC,IAAY,EAAE,MAA8C;IAC7F,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,MAAM,CAAC,EAAE,CAAC,QAAQ,KAAK,IAAI;QAC3C,MAAM,IAAI,KAAK,CAAC,mBAAmB,IAAI,uCAAuC,CAAC,CAAA;IACjF,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,QAAQ,KAAK,IAAI;QACjD,MAAM,IAAI,KAAK,CAAC,mBAAmB,IAAI,0CAA0C,CAAC,CAAA;AACtF,CAAC;AAED,SAAS,eAAe,CACtB,IAAY,EACZ,KAAa,EACb,MAAS,EACT,OAAmB;IAEnB,MAAM,KAAK,GAAG,OAAO,EAAE,CAAC,eAAe,EAAK,CAAC,CAAA;IAC7C,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE;QAChD,SAAS,EAAE,KAAK;QAChB,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC1C,CAAC,CAAA;IACF,OAAO;QACL,IAAI;QACJ,MAAM;QACN,MAAM;QACN,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,eAAe,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC;KACxD,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,MAAM,CACJ,IAAY,EACZ,KAAa,EACb,MAAS;QAET,kBAAkB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QAChC,OAAO,eAAe,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,CAAA;IAC7C,CAAC;CACF,CAAA"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { FieldsMap, InferFieldValue } from "../collection/types.js";
|
|
2
|
+
import type { FilterNode } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Operand returned by `Filter.field(name)` — provides comparison methods
|
|
5
|
+
* that each produce a `FilterNode`.
|
|
6
|
+
*
|
|
7
|
+
* The type parameter `V` constrains the value type for comparisons:
|
|
8
|
+
* when used through `FilterBuilder<F>.field(name)`, `V` is the inferred
|
|
9
|
+
* TypeScript type of the field (string, number, boolean, etc.). When used
|
|
10
|
+
* through the untyped `Filter.field(name)`, `V` is `unknown`.
|
|
11
|
+
*
|
|
12
|
+
* Available comparisons:
|
|
13
|
+
* - `.eq(v)` / `.neq(v)` — equality / inequality
|
|
14
|
+
* - `.gt(v)` / `.gte(v)` — greater than / greater or equal
|
|
15
|
+
* - `.lt(v)` / `.lte(v)` — less than / less or equal
|
|
16
|
+
* - `.in(values)` — membership in a list
|
|
17
|
+
* - `.like(pattern)` — SQL LIKE pattern match (always string-valued)
|
|
18
|
+
* - `.null()` / `.notNull()` — IS NULL / IS NOT NULL (no value argument)
|
|
19
|
+
* - `.between(lo, hi)` — compound of `.gte(lo)` + `.lte(hi)`
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* import { Filter } from "@gettersethya/mira-client"
|
|
23
|
+
*
|
|
24
|
+
* Filter.field("age").gte(18)
|
|
25
|
+
* Filter.field("title").like("%effect%")
|
|
26
|
+
* Filter.field("tags").in(["a", "b", "c"])
|
|
27
|
+
* Filter.field("deletedAt").null()
|
|
28
|
+
* Filter.field("price").between(10, 100)
|
|
29
|
+
*
|
|
30
|
+
* @see Filter — entry point to create FieldFilterOperand
|
|
31
|
+
* @see FilterBuilder — typed variant with field-name and value-type safety
|
|
32
|
+
*/
|
|
33
|
+
export type FieldFilterOperand<V = unknown> = {
|
|
34
|
+
eq(value: V): FilterNode;
|
|
35
|
+
neq(value: V): FilterNode;
|
|
36
|
+
gt(value: V): FilterNode;
|
|
37
|
+
gte(value: V): FilterNode;
|
|
38
|
+
lt(value: V): FilterNode;
|
|
39
|
+
lte(value: V): FilterNode;
|
|
40
|
+
in(values: ReadonlyArray<V>): FilterNode;
|
|
41
|
+
like(value: string): FilterNode;
|
|
42
|
+
null(): FilterNode;
|
|
43
|
+
notNull(): FilterNode;
|
|
44
|
+
between(lo: V, hi: V): FilterNode;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Typed filter builder constrained to a specific `FieldsMap`.
|
|
48
|
+
* Provides the same methods as `Filter` but narrows field names and value
|
|
49
|
+
* types according to the collection's field definitions.
|
|
50
|
+
*
|
|
51
|
+
* Used inside `CollectionClient.getList({ filter: (f) => ... })` callbacks.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* interface PostFields { title: FieldDef & { kind: "text" }; views: FieldDef & { kind: "number" } }
|
|
55
|
+
* const f: FilterBuilder<PostFields>
|
|
56
|
+
*
|
|
57
|
+
* f.field("title").eq("hello") // typed — value is string
|
|
58
|
+
* f.field("views").gte(100) // typed — value is number
|
|
59
|
+
* f.field("views").eq("bad") // type error — expects number
|
|
60
|
+
* f.field("nonexistent").eq(1) // type error — field does not exist
|
|
61
|
+
*
|
|
62
|
+
* @see Filter — untyped version of the same DSL
|
|
63
|
+
* @see FieldFilterOperand — the return type of .field()
|
|
64
|
+
*/
|
|
65
|
+
export type FilterBuilder<F extends FieldsMap = FieldsMap> = {
|
|
66
|
+
field<K extends keyof F & string>(name: K): FieldFilterOperand<InferFieldValue<F[K]>>;
|
|
67
|
+
and(left: FilterNode, right: FilterNode): FilterNode;
|
|
68
|
+
or(left: FilterNode, right: FilterNode): FilterNode;
|
|
69
|
+
not(node: FilterNode): FilterNode;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Filter DSL builder for constructing `FilterNode` expressions.
|
|
73
|
+
*
|
|
74
|
+
* Use `Filter.field(name)` to start a comparison chain, then call a comparison
|
|
75
|
+
* method (`.eq()`, `.neq()`, `.gt()`, etc.) to produce a `FilterNode`.
|
|
76
|
+
* Combine nodes with `Filter.and()`, `Filter.or()`, `Filter.not()`.
|
|
77
|
+
*
|
|
78
|
+
* This is the untyped variant — field names are plain strings and values are
|
|
79
|
+
* `unknown`. For type-safe field access, use `FilterBuilder<F>` via the
|
|
80
|
+
* `CollectionClient` callback pattern.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* import { Filter } from "@gettersethya/mira-client"
|
|
84
|
+
*
|
|
85
|
+
* // Simple equality
|
|
86
|
+
* const published = Filter.field("published").eq(true)
|
|
87
|
+
*
|
|
88
|
+
* // Range check
|
|
89
|
+
* const popular = Filter.and(
|
|
90
|
+
* Filter.field("views").gte(100),
|
|
91
|
+
* Filter.field("views").lte(10_000)
|
|
92
|
+
* )
|
|
93
|
+
*
|
|
94
|
+
* // In-list
|
|
95
|
+
* const draftOrArchived = Filter.field("status").in(["draft", "archived"])
|
|
96
|
+
*
|
|
97
|
+
* // LIKE pattern
|
|
98
|
+
* const search = Filter.field("title").like("%effect%")
|
|
99
|
+
*
|
|
100
|
+
* // Null checks
|
|
101
|
+
* const hasDeletedAt = Filter.field("deletedAt").null()
|
|
102
|
+
* const notDeleted = Filter.field("deletedAt").notNull()
|
|
103
|
+
*
|
|
104
|
+
* // Complex combination
|
|
105
|
+
* const query = Filter.and(
|
|
106
|
+
* Filter.field("published").eq(true),
|
|
107
|
+
* Filter.or(
|
|
108
|
+
* Filter.field("category").eq("tech"),
|
|
109
|
+
* Filter.field("tags").contains("effect")
|
|
110
|
+
* )
|
|
111
|
+
* )
|
|
112
|
+
*
|
|
113
|
+
* @see FilterNode — the AST node produced by filter methods
|
|
114
|
+
* @see FilterBuilder — typed variant used in CollectionClient methods
|
|
115
|
+
* @see filterNodeToWhereClause — compiles FilterNode to SQL
|
|
116
|
+
*/
|
|
117
|
+
export declare const Filter: {
|
|
118
|
+
/** Start a filter chain for a field by name. Returns a `FieldFilterOperand` with comparison methods. */
|
|
119
|
+
field: <K extends string>(name: K) => FieldFilterOperand<unknown>;
|
|
120
|
+
/** Combine two filter nodes with logical AND. Produces `(left) AND (right)` in SQL. */
|
|
121
|
+
and: (left: FilterNode, right: FilterNode) => FilterNode;
|
|
122
|
+
/** Combine two filter nodes with logical OR. Produces `(left) OR (right)` in SQL. */
|
|
123
|
+
or: (left: FilterNode, right: FilterNode) => FilterNode;
|
|
124
|
+
/** Negate a filter node with logical NOT. Produces `NOT (node)` in SQL. */
|
|
125
|
+
not: (node: FilterNode) => FilterNode;
|
|
126
|
+
};
|
|
127
|
+
//# sourceMappingURL=builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../../src/filter/builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AACvE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,MAAM,kBAAkB,CAAC,CAAC,GAAG,OAAO,IAAI;IAC5C,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,UAAU,CAAA;IACxB,GAAG,CAAC,KAAK,EAAE,CAAC,GAAG,UAAU,CAAA;IACzB,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,UAAU,CAAA;IACxB,GAAG,CAAC,KAAK,EAAE,CAAC,GAAG,UAAU,CAAA;IACzB,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,UAAU,CAAA;IACxB,GAAG,CAAC,KAAK,EAAE,CAAC,GAAG,UAAU,CAAA;IACzB,EAAE,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,UAAU,CAAA;IACxC,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAAA;IAC/B,IAAI,IAAI,UAAU,CAAA;IAClB,OAAO,IAAI,UAAU,CAAA;IACrB,OAAO,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,UAAU,CAAA;CAClC,CAAA;AAkBD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,SAAS,GAAG,SAAS,IAAI;IAC3D,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,kBAAkB,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACrF,GAAG,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,GAAG,UAAU,CAAA;IACpD,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,GAAG,UAAU,CAAA;IACnD,GAAG,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,CAAA;CAClC,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,eAAO,MAAM,MAAM;IACjB,wGAAwG;YAChG,CAAC,SAAS,MAAM,QAAQ,CAAC,KAAG,kBAAkB,CAAC,OAAO,CAAC;IAG/D,uFAAuF;gBAC3E,UAAU,SAAS,UAAU,KAAG,UAAU;IAGtD,qFAAqF;eAC1E,UAAU,SAAS,UAAU,KAAG,UAAU;IAGrD,2EAA2E;gBAC/D,UAAU,KAAG,UAAU;CAEpC,CAAA"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
function toFieldOperand(field) {
|
|
2
|
+
return {
|
|
3
|
+
eq: (value) => ({ op: "eq", field, value }),
|
|
4
|
+
neq: (value) => ({ op: "neq", field, value }),
|
|
5
|
+
gt: (value) => ({ op: "gt", field, value }),
|
|
6
|
+
gte: (value) => ({ op: "gte", field, value }),
|
|
7
|
+
lt: (value) => ({ op: "lt", field, value }),
|
|
8
|
+
lte: (value) => ({ op: "lte", field, value }),
|
|
9
|
+
in: (values) => ({ op: "in", field, values }),
|
|
10
|
+
like: (value) => ({ op: "like", field, value }),
|
|
11
|
+
null: () => ({ op: "null", field }),
|
|
12
|
+
notNull: () => ({ op: "not_null", field }),
|
|
13
|
+
between: (lo, hi) => ({ op: "and", left: { op: "gte", field, value: lo }, right: { op: "lte", field, value: hi } })
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Filter DSL builder for constructing `FilterNode` expressions.
|
|
18
|
+
*
|
|
19
|
+
* Use `Filter.field(name)` to start a comparison chain, then call a comparison
|
|
20
|
+
* method (`.eq()`, `.neq()`, `.gt()`, etc.) to produce a `FilterNode`.
|
|
21
|
+
* Combine nodes with `Filter.and()`, `Filter.or()`, `Filter.not()`.
|
|
22
|
+
*
|
|
23
|
+
* This is the untyped variant — field names are plain strings and values are
|
|
24
|
+
* `unknown`. For type-safe field access, use `FilterBuilder<F>` via the
|
|
25
|
+
* `CollectionClient` callback pattern.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* import { Filter } from "@gettersethya/mira-client"
|
|
29
|
+
*
|
|
30
|
+
* // Simple equality
|
|
31
|
+
* const published = Filter.field("published").eq(true)
|
|
32
|
+
*
|
|
33
|
+
* // Range check
|
|
34
|
+
* const popular = Filter.and(
|
|
35
|
+
* Filter.field("views").gte(100),
|
|
36
|
+
* Filter.field("views").lte(10_000)
|
|
37
|
+
* )
|
|
38
|
+
*
|
|
39
|
+
* // In-list
|
|
40
|
+
* const draftOrArchived = Filter.field("status").in(["draft", "archived"])
|
|
41
|
+
*
|
|
42
|
+
* // LIKE pattern
|
|
43
|
+
* const search = Filter.field("title").like("%effect%")
|
|
44
|
+
*
|
|
45
|
+
* // Null checks
|
|
46
|
+
* const hasDeletedAt = Filter.field("deletedAt").null()
|
|
47
|
+
* const notDeleted = Filter.field("deletedAt").notNull()
|
|
48
|
+
*
|
|
49
|
+
* // Complex combination
|
|
50
|
+
* const query = Filter.and(
|
|
51
|
+
* Filter.field("published").eq(true),
|
|
52
|
+
* Filter.or(
|
|
53
|
+
* Filter.field("category").eq("tech"),
|
|
54
|
+
* Filter.field("tags").contains("effect")
|
|
55
|
+
* )
|
|
56
|
+
* )
|
|
57
|
+
*
|
|
58
|
+
* @see FilterNode — the AST node produced by filter methods
|
|
59
|
+
* @see FilterBuilder — typed variant used in CollectionClient methods
|
|
60
|
+
* @see filterNodeToWhereClause — compiles FilterNode to SQL
|
|
61
|
+
*/
|
|
62
|
+
export const Filter = {
|
|
63
|
+
/** Start a filter chain for a field by name. Returns a `FieldFilterOperand` with comparison methods. */
|
|
64
|
+
field: (name) => toFieldOperand(name),
|
|
65
|
+
/** Combine two filter nodes with logical AND. Produces `(left) AND (right)` in SQL. */
|
|
66
|
+
and: (left, right) => ({ op: "and", left, right }),
|
|
67
|
+
/** Combine two filter nodes with logical OR. Produces `(left) OR (right)` in SQL. */
|
|
68
|
+
or: (left, right) => ({ op: "or", left, right }),
|
|
69
|
+
/** Negate a filter node with logical NOT. Produces `NOT (node)` in SQL. */
|
|
70
|
+
not: (node) => ({ op: "not", node })
|
|
71
|
+
};
|
|
72
|
+
//# sourceMappingURL=builder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builder.js","sourceRoot":"","sources":["../../../src/filter/builder.ts"],"names":[],"mappings":"AA+CA,SAAS,cAAc,CAAC,KAAa;IACnC,OAAO;QACL,EAAE,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAa,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;QACpD,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,KAAc,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;QACtD,EAAE,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAa,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;QACpD,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,KAAc,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;QACtD,EAAE,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAa,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;QACpD,GAAG,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,KAAc,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;QACtD,EAAE,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAa,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QACtD,IAAI,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,MAAe,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;QACxD,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,MAAe,EAAE,KAAK,EAAE,CAAC;QAC5C,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,UAAmB,EAAE,KAAK,EAAE,CAAC;QACnD,OAAO,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,KAAc,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,KAAc,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,KAAc,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,CAAC;KAC/I,CAAA;AACH,CAAC;AA4BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,wGAAwG;IACxG,KAAK,EAAE,CAAmB,IAAO,EAA+B,EAAE,CAChE,cAAc,CAAC,IAAI,CAAC;IAEtB,uFAAuF;IACvF,GAAG,EAAE,CAAC,IAAgB,EAAE,KAAiB,EAAc,EAAE,CACvD,CAAC,EAAE,EAAE,EAAE,KAAc,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IAEvC,qFAAqF;IACrF,EAAE,EAAE,CAAC,IAAgB,EAAE,KAAiB,EAAc,EAAE,CACtD,CAAC,EAAE,EAAE,EAAE,IAAa,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IAEtC,2EAA2E;IAC3E,GAAG,EAAE,CAAC,IAAgB,EAAc,EAAE,CACpC,CAAC,EAAE,EAAE,EAAE,KAAc,EAAE,IAAI,EAAE,CAAC;CACjC,CAAA"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { CollectionSchema } from "../collection/types.js";
|
|
3
|
+
import { ValidationError } from "../collection/errors.js";
|
|
4
|
+
import type { FilterNode, WhereClause } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Compile a `FilterNode` AST into a `WhereClause` (`{ sql, params }`) suitable
|
|
7
|
+
* for splicing into an Effect SQL template literal via `unsafeFragment`.
|
|
8
|
+
*
|
|
9
|
+
* Validates each referenced field against the collection schema. Unknown fields
|
|
10
|
+
* produce a `ValidationError` in the Effect error channel.
|
|
11
|
+
*
|
|
12
|
+
* Field names are SQL-identifier-quoted (double-quoted as `t."fieldName"`).
|
|
13
|
+
* Values use `?`-style positional placeholders with params in the returned array.
|
|
14
|
+
*
|
|
15
|
+
* Edge cases:
|
|
16
|
+
* - `IN ()` with an empty array produces `1 = 0` (always false) instead of
|
|
17
|
+
* invalid SQL like `IN ()`.
|
|
18
|
+
* - The `like` pattern is passed through verbatim — the caller is responsible
|
|
19
|
+
* for adding `%` wildcards.
|
|
20
|
+
* - All column references use the hardcoded table alias `t`.
|
|
21
|
+
*
|
|
22
|
+
* @param node - The filter AST to compile
|
|
23
|
+
* @param schema - The collection schema (used for field validation)
|
|
24
|
+
* @param collectionName - Collection name (used only in error messages)
|
|
25
|
+
* @returns Effect that resolves to a WhereClause, or fails with ValidationError
|
|
26
|
+
* if any field name in the filter does not exist in the schema
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* import { Filter, filterNodeToWhereClause } from "@gettersethya/mira-client"
|
|
30
|
+
* import type { CollectionSchema } from "@gettersethya/mira-client"
|
|
31
|
+
*
|
|
32
|
+
* const schema: CollectionSchema = {
|
|
33
|
+
* type: "object",
|
|
34
|
+
* properties: { title: { type: "string" } }
|
|
35
|
+
* } as CollectionSchema
|
|
36
|
+
*
|
|
37
|
+
* const effect = filterNodeToWhereClause(
|
|
38
|
+
* Filter.field("title").eq("hello"),
|
|
39
|
+
* schema,
|
|
40
|
+
* "posts"
|
|
41
|
+
* )
|
|
42
|
+
* // Resolves to: { sql: 't."title" = ?', params: ["hello"] }
|
|
43
|
+
*
|
|
44
|
+
* @see FilterNode — the AST type consumed by this function
|
|
45
|
+
* @see WhereClause — the output type
|
|
46
|
+
*/
|
|
47
|
+
export declare function filterNodeToWhereClause(node: FilterNode, schema: CollectionSchema, collectionName: string): Effect.Effect<WhereClause, ValidationError>;
|
|
48
|
+
//# sourceMappingURL=compiler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compiler.d.ts","sourceRoot":"","sources":["../../../src/filter/compiler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAEzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,UAAU,EAChB,MAAM,EAAE,gBAAgB,EACxB,cAAc,EAAE,MAAM,GACrB,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,eAAe,CAAC,CAE7C"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { ValidationError } from "../collection/errors.js";
|
|
3
|
+
/**
|
|
4
|
+
* Compile a `FilterNode` AST into a `WhereClause` (`{ sql, params }`) suitable
|
|
5
|
+
* for splicing into an Effect SQL template literal via `unsafeFragment`.
|
|
6
|
+
*
|
|
7
|
+
* Validates each referenced field against the collection schema. Unknown fields
|
|
8
|
+
* produce a `ValidationError` in the Effect error channel.
|
|
9
|
+
*
|
|
10
|
+
* Field names are SQL-identifier-quoted (double-quoted as `t."fieldName"`).
|
|
11
|
+
* Values use `?`-style positional placeholders with params in the returned array.
|
|
12
|
+
*
|
|
13
|
+
* Edge cases:
|
|
14
|
+
* - `IN ()` with an empty array produces `1 = 0` (always false) instead of
|
|
15
|
+
* invalid SQL like `IN ()`.
|
|
16
|
+
* - The `like` pattern is passed through verbatim — the caller is responsible
|
|
17
|
+
* for adding `%` wildcards.
|
|
18
|
+
* - All column references use the hardcoded table alias `t`.
|
|
19
|
+
*
|
|
20
|
+
* @param node - The filter AST to compile
|
|
21
|
+
* @param schema - The collection schema (used for field validation)
|
|
22
|
+
* @param collectionName - Collection name (used only in error messages)
|
|
23
|
+
* @returns Effect that resolves to a WhereClause, or fails with ValidationError
|
|
24
|
+
* if any field name in the filter does not exist in the schema
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* import { Filter, filterNodeToWhereClause } from "@gettersethya/mira-client"
|
|
28
|
+
* import type { CollectionSchema } from "@gettersethya/mira-client"
|
|
29
|
+
*
|
|
30
|
+
* const schema: CollectionSchema = {
|
|
31
|
+
* type: "object",
|
|
32
|
+
* properties: { title: { type: "string" } }
|
|
33
|
+
* } as CollectionSchema
|
|
34
|
+
*
|
|
35
|
+
* const effect = filterNodeToWhereClause(
|
|
36
|
+
* Filter.field("title").eq("hello"),
|
|
37
|
+
* schema,
|
|
38
|
+
* "posts"
|
|
39
|
+
* )
|
|
40
|
+
* // Resolves to: { sql: 't."title" = ?', params: ["hello"] }
|
|
41
|
+
*
|
|
42
|
+
* @see FilterNode — the AST type consumed by this function
|
|
43
|
+
* @see WhereClause — the output type
|
|
44
|
+
*/
|
|
45
|
+
export function filterNodeToWhereClause(node, schema, collectionName) {
|
|
46
|
+
return compileNode(node, schema, collectionName);
|
|
47
|
+
}
|
|
48
|
+
function compileNode(node, schema, collection) {
|
|
49
|
+
switch (node.op) {
|
|
50
|
+
case "eq":
|
|
51
|
+
case "neq":
|
|
52
|
+
case "gt":
|
|
53
|
+
case "gte":
|
|
54
|
+
case "lt":
|
|
55
|
+
case "lte":
|
|
56
|
+
return compileComparison(node, collection, schema);
|
|
57
|
+
case "in":
|
|
58
|
+
return compileIn(node, collection, schema);
|
|
59
|
+
case "like":
|
|
60
|
+
return compileLike(node, collection, schema);
|
|
61
|
+
case "null":
|
|
62
|
+
case "not_null":
|
|
63
|
+
return compileNull(node, collection, schema);
|
|
64
|
+
case "and":
|
|
65
|
+
case "or":
|
|
66
|
+
return compileLogical(node, schema, collection);
|
|
67
|
+
case "not":
|
|
68
|
+
return compileNot(node, schema, collection);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function validateField(field, collection, schema) {
|
|
72
|
+
if (!schema.properties[field]) {
|
|
73
|
+
return Effect.fail(new ValidationError({
|
|
74
|
+
collection,
|
|
75
|
+
issues: [`Unknown field in filter: "${field}"`]
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
return Effect.void;
|
|
79
|
+
}
|
|
80
|
+
function compileComparison(node, collection, schema) {
|
|
81
|
+
const opMap = {
|
|
82
|
+
eq: "=",
|
|
83
|
+
neq: "!=",
|
|
84
|
+
gt: ">",
|
|
85
|
+
gte: ">=",
|
|
86
|
+
lt: "<",
|
|
87
|
+
lte: "<="
|
|
88
|
+
};
|
|
89
|
+
return Effect.gen(function* () {
|
|
90
|
+
yield* validateField(node.field, collection, schema);
|
|
91
|
+
return { sql: `t."${node.field}" ${opMap[node.op]} ?`, params: [node.value] };
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function compileIn(node, collection, schema) {
|
|
95
|
+
return Effect.gen(function* () {
|
|
96
|
+
yield* validateField(node.field, collection, schema);
|
|
97
|
+
if (node.values.length === 0)
|
|
98
|
+
return { sql: "1 = 0", params: [] };
|
|
99
|
+
const placeholders = node.values.map(() => "?").join(", ");
|
|
100
|
+
return { sql: `t."${node.field}" IN (${placeholders})`, params: [...node.values] };
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function compileLike(node, collection, schema) {
|
|
104
|
+
return Effect.gen(function* () {
|
|
105
|
+
yield* validateField(node.field, collection, schema);
|
|
106
|
+
return { sql: `t."${node.field}" LIKE ?`, params: [node.value] };
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function compileNull(node, collection, schema) {
|
|
110
|
+
return Effect.gen(function* () {
|
|
111
|
+
yield* validateField(node.field, collection, schema);
|
|
112
|
+
if (node.op === "null") {
|
|
113
|
+
return { sql: `t."${node.field}" IS NULL`, params: [] };
|
|
114
|
+
}
|
|
115
|
+
return { sql: `t."${node.field}" IS NOT NULL`, params: [] };
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function compileLogical(node, schema, collection) {
|
|
119
|
+
return Effect.gen(function* () {
|
|
120
|
+
const left = yield* compileNode(node.left, schema, collection);
|
|
121
|
+
const right = yield* compileNode(node.right, schema, collection);
|
|
122
|
+
const op = node.op.toUpperCase();
|
|
123
|
+
return { sql: `(${left.sql}) ${op} (${right.sql})`, params: [...left.params, ...right.params] };
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function compileNot(node, schema, collection) {
|
|
127
|
+
return Effect.gen(function* () {
|
|
128
|
+
const inner = yield* compileNode(node.node, schema, collection);
|
|
129
|
+
return { sql: `NOT (${inner.sql})`, params: inner.params };
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=compiler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compiler.js","sourceRoot":"","sources":["../../../src/filter/compiler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { Filter } from "./builder.js";
|
|
2
|
+
export { filterNodeToWhereClause } from "./compiler.js";
|
|
3
|
+
export { FilterNodeSchema } from "./types.js";
|
|
4
|
+
export type { FilterBuilder, FieldFilterOperand } from "./builder.js";
|
|
5
|
+
export type { FilterNode, WhereClause } from "./types.js";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/filter/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AACrC,OAAO,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAA;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAC7C,YAAY,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AACrE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/filter/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AACrC,OAAO,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAA;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
/**
|
|
3
|
+
* A pre-compiled SQL WHERE fragment with positional `?` placeholders.
|
|
4
|
+
*
|
|
5
|
+
* Produced by `filterNodeToWhereClause()` and consumed by the repository layer.
|
|
6
|
+
* The `sql` string uses `?`-style placeholders; `params` supplies the values
|
|
7
|
+
* in positional order. Pass to `unsafeFragment(w.sql, w.params)` from
|
|
8
|
+
* `@effect/sql/Statement` to splice into a template literal.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* // Output of compiler for Filter.field("title").eq("hello"):
|
|
12
|
+
* const w: WhereClause = { sql: 't."title" = ?', params: ["hello"] }
|
|
13
|
+
*
|
|
14
|
+
* @see FilterNode — the AST type that compiles to WhereClause
|
|
15
|
+
* @see filterNodeToWhereClause — produces WhereClause from FilterNode
|
|
16
|
+
*/
|
|
17
|
+
export type WhereClause = {
|
|
18
|
+
sql: string;
|
|
19
|
+
params: ReadonlyArray<unknown>;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* AST node for filter expressions. Discriminated union with 6 variant groups:
|
|
23
|
+
*
|
|
24
|
+
* | Variants | Fields | Description |
|
|
25
|
+
* |---|---|---|
|
|
26
|
+
* | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | `field`, `value` | Value comparisons |
|
|
27
|
+
* | `in` | `field`, `values` | Membership test |
|
|
28
|
+
* | `like` | `field`, `value` | SQL LIKE pattern match |
|
|
29
|
+
* | `null`, `not_null` | `field` | Null checks (no value operand) |
|
|
30
|
+
* | `and`, `or` | `left`, `right` | Binary logical combinators |
|
|
31
|
+
* | `not` | `node` | Unary negation |
|
|
32
|
+
*
|
|
33
|
+
* Construct via `Filter.field(name).eq(value)` or `Filter.and(left, right)`.
|
|
34
|
+
* Consumed by `filterNodeToWhereClause()` to produce SQL WHERE clauses.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* import { Filter } from "@gettersethya/mira-client"
|
|
38
|
+
*
|
|
39
|
+
* // Single comparison
|
|
40
|
+
* const node: FilterNode = Filter.field("title").eq("hello")
|
|
41
|
+
*
|
|
42
|
+
* // Logical combination
|
|
43
|
+
* const complex: FilterNode = Filter.and(
|
|
44
|
+
* Filter.field("published").eq(true),
|
|
45
|
+
* Filter.field("views").gte(100)
|
|
46
|
+
* )
|
|
47
|
+
*
|
|
48
|
+
* // Null check
|
|
49
|
+
* const deleted: FilterNode = Filter.field("deletedAt").notNull()
|
|
50
|
+
*
|
|
51
|
+
* @see Filter — builder DSL for constructing FilterNode values
|
|
52
|
+
* @see filterNodeToWhereClause — compiles FilterNode to SQL
|
|
53
|
+
*/
|
|
54
|
+
export type FilterNode = {
|
|
55
|
+
op: "eq" | "neq" | "gt" | "gte" | "lt" | "lte";
|
|
56
|
+
field: string;
|
|
57
|
+
value: unknown;
|
|
58
|
+
} | {
|
|
59
|
+
op: "in";
|
|
60
|
+
field: string;
|
|
61
|
+
values: ReadonlyArray<unknown>;
|
|
62
|
+
} | {
|
|
63
|
+
op: "like";
|
|
64
|
+
field: string;
|
|
65
|
+
value: string;
|
|
66
|
+
} | {
|
|
67
|
+
op: "null" | "not_null";
|
|
68
|
+
field: string;
|
|
69
|
+
} | {
|
|
70
|
+
op: "and" | "or";
|
|
71
|
+
left: FilterNode;
|
|
72
|
+
right: FilterNode;
|
|
73
|
+
} | {
|
|
74
|
+
op: "not";
|
|
75
|
+
node: FilterNode;
|
|
76
|
+
};
|
|
77
|
+
declare let _filterNodeSchema: Schema.Schema<FilterNode>;
|
|
78
|
+
/**
|
|
79
|
+
* Effect Schema for `FilterNode`. Used to decode filter query parameters
|
|
80
|
+
* from HTTP request JSON bodies.
|
|
81
|
+
*
|
|
82
|
+
* The schema is self-referential (recursive via `Schema.suspend`) to handle
|
|
83
|
+
* nested `and`/`or`/`not` nodes. This is why the underlying `let` variable
|
|
84
|
+
* exists — the recursive `suspend` needs a forward reference.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* import { Schema } from "effect"
|
|
88
|
+
*
|
|
89
|
+
* const raw = { op: "eq", field: "title", value: "hello" }
|
|
90
|
+
* const filter = Schema.decodeSync(FilterNodeSchema)(raw)
|
|
91
|
+
* // filter: FilterNode — { op: "eq", field: "title", value: "hello" }
|
|
92
|
+
*
|
|
93
|
+
* @see FilterNode — the TypeScript type this schema decodes to
|
|
94
|
+
*/
|
|
95
|
+
export { _filterNodeSchema as FilterNodeSchema };
|
|
96
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/filter/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAE/B;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;CAC/B,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,MAAM,UAAU,GAClB;IAAE,EAAE,EAAE,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GACjF;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;CAAE,GAC3D;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,EAAE,EAAE,MAAM,GAAG,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,EAAE,EAAE,KAAK,GAAG,IAAI,CAAC;IAAC,IAAI,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,UAAU,CAAA;CAAE,GACzD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,CAAA;AAEnC,QAAA,IAAI,iBAAiB,EAAE,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;AAiChD;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,iBAAiB,IAAI,gBAAgB,EAAE,CAAA"}
|