@petrarca/sonnet-core 0.1.0

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.
@@ -0,0 +1,119 @@
1
+ /**
2
+ * FilterExpr -- lightweight boolean expression tree for search and filtering.
3
+ *
4
+ * Designed to start simple (a flat AND list of terms) and grow to support
5
+ * full boolean queries (AND, OR, NOT, grouping) without breaking callers.
6
+ *
7
+ * The component always emits a FilterExpr. Callers that talk to a flat
8
+ * server API today use flattenFilterExpr() to extract the terms. When the
9
+ * server gains AST support, callers drop flattenFilterExpr and pass the
10
+ * expression directly -- no component changes required.
11
+ *
12
+ * Extractable to @petrarca/sonnet-core: no app-specific imports.
13
+ */
14
+ /** A single field/value filter term -- the leaf node of a FilterExpr. */
15
+ interface FilterTerm {
16
+ type: "term";
17
+ /** Schema property key -- used as the query param name sent to the server. */
18
+ key: string;
19
+ /** The filter value. */
20
+ value: string;
21
+ }
22
+ /** Conjunction: all children must match. */
23
+ interface FilterAnd {
24
+ type: "and";
25
+ children: FilterExpr[];
26
+ }
27
+ /** Disjunction: at least one child must match. */
28
+ interface FilterOr {
29
+ type: "or";
30
+ children: FilterExpr[];
31
+ }
32
+ /** Negation: child must not match. */
33
+ interface FilterNot {
34
+ type: "not";
35
+ child: FilterExpr;
36
+ }
37
+ /**
38
+ * Explicit grouping (parentheses). Semantically identical to its child --
39
+ * used to preserve user intent when round-tripping the expression.
40
+ */
41
+ interface FilterGroup {
42
+ type: "group";
43
+ child: FilterExpr;
44
+ }
45
+ /** The full expression type. */
46
+ type FilterExpr = FilterTerm | FilterAnd | FilterOr | FilterNot | FilterGroup;
47
+ /** An empty expression -- no filters active. */
48
+ declare const EMPTY_FILTER: FilterExpr;
49
+
50
+ /**
51
+ * Utilities for working with FilterExpr trees.
52
+ *
53
+ * Extractable to @petrarca/sonnet-core: no app-specific imports.
54
+ */
55
+
56
+ /**
57
+ * Return all term nodes from the expression as FilterTerm objects, depth-first.
58
+ *
59
+ * Ignores the boolean structure (AND / OR / NOT / GROUP).
60
+ */
61
+ declare function collectTerms(expr: FilterExpr): FilterTerm[];
62
+ /**
63
+ * Collect all term nodes from a FilterExpr tree as flat key/value pairs.
64
+ *
65
+ * Use this when talking to a server API that accepts only flat key/value
66
+ * filter params. When the server gains AST support, pass the raw FilterExpr
67
+ * instead and drop this call.
68
+ *
69
+ * @example
70
+ * const terms = flattenFilterExpr(expr);
71
+ * // [{ key: "name", value: "acme" }, { key: "status", value: "active" }]
72
+ * const params = Object.fromEntries(terms.map(t => [t.key, t.value]));
73
+ */
74
+ declare function flattenFilterExpr(expr: FilterExpr): {
75
+ key: string;
76
+ value: string;
77
+ }[];
78
+ /**
79
+ * Return true when the expression contains no term nodes.
80
+ */
81
+ declare function isEmptyFilterExpr(expr: FilterExpr): boolean;
82
+ /**
83
+ * Serialize a FilterExpr to the server's q= expression syntax.
84
+ *
85
+ * Produces strings like `code ~ "aspirin" OR display ~ "aspirin"`.
86
+ * All terms use the `~` (LIKE / substring) operator -- the FilterTerm
87
+ * type carries no operator field; callers that need exact-match or other
88
+ * operators should construct the q= string directly.
89
+ */
90
+ declare function serializeFilterExpr(expr: FilterExpr): string;
91
+ /**
92
+ * Build a typed API params object from a FilterExpr and pagination state.
93
+ *
94
+ * Used by list and expansion pages to convert Zustand store state into
95
+ * query params for the server. The base object carries pagination keys
96
+ * (limit / offset); filter terms from the expression are merged in.
97
+ *
98
+ * When the expression is a flat AND of terms, each term becomes a flat
99
+ * query param (backwards compatible with existing server endpoints).
100
+ * When the expression contains OR/NOT/Group nodes, it is serialized
101
+ * as a `q=` string using the server's query expression syntax.
102
+ *
103
+ * @example
104
+ * const params = buildListParams<ValueSetListParams>(
105
+ * filterExpr,
106
+ * { limit: pagination.pageSize, offset: pagination.pageIndex * pagination.pageSize },
107
+ * );
108
+ */
109
+ declare function buildListParams<T extends object>(expr: FilterExpr, base: T): T;
110
+ /**
111
+ * Build a flat AND expression from an array of key/value pairs.
112
+ * Convenience for constructing the initial state from existing filter params.
113
+ */
114
+ declare function termsToFilterExpr(terms: {
115
+ key: string;
116
+ value: string;
117
+ }[]): FilterExpr;
118
+
119
+ export { EMPTY_FILTER, type FilterAnd, type FilterExpr, type FilterGroup, type FilterNot, type FilterOr, type FilterTerm, buildListParams, collectTerms, flattenFilterExpr, isEmptyFilterExpr, serializeFilterExpr, termsToFilterExpr };
@@ -0,0 +1,82 @@
1
+ // src/search/types.ts
2
+ var EMPTY_FILTER = { type: "and", children: [] };
3
+
4
+ // src/search/filterExpr.ts
5
+ function collectTerms(expr) {
6
+ const out = [];
7
+ collectTermNodes(expr, out);
8
+ return out;
9
+ }
10
+ function collectTermNodes(expr, out) {
11
+ switch (expr.type) {
12
+ case "term":
13
+ out.push(expr);
14
+ break;
15
+ case "and":
16
+ case "or":
17
+ for (const child of expr.children) collectTermNodes(child, out);
18
+ break;
19
+ case "not":
20
+ case "group":
21
+ collectTermNodes(expr.child, out);
22
+ break;
23
+ }
24
+ }
25
+ function flattenFilterExpr(expr) {
26
+ return collectTerms(expr).map(({ key, value }) => ({ key, value }));
27
+ }
28
+ function isEmptyFilterExpr(expr) {
29
+ return collectTerms(expr).length === 0;
30
+ }
31
+ function isFlatAnd(expr) {
32
+ if (expr.type === "term") return true;
33
+ if (expr.type === "and") return expr.children.every((c) => c.type === "term");
34
+ return false;
35
+ }
36
+ function serializeFilterExpr(expr) {
37
+ switch (expr.type) {
38
+ case "term": {
39
+ const escaped = expr.value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
40
+ return `${expr.key} ~ "${escaped}"`;
41
+ }
42
+ case "and":
43
+ return expr.children.map((c) => serializeFilterExpr(c)).join(" AND ");
44
+ case "or":
45
+ return expr.children.map((c) => serializeFilterExpr(c)).join(" OR ");
46
+ case "not":
47
+ return `NOT (${serializeFilterExpr(expr.child)})`;
48
+ case "group":
49
+ return `(${serializeFilterExpr(expr.child)})`;
50
+ }
51
+ }
52
+ function buildListParams(expr, base) {
53
+ if (isEmptyFilterExpr(expr)) return base;
54
+ if (isFlatAnd(expr)) {
55
+ const pairs = flattenFilterExpr(expr);
56
+ const extra = Object.fromEntries(
57
+ pairs.filter(({ key }) => !(key in base)).map(({ key, value }) => [key, value])
58
+ );
59
+ return { ...base, ...extra };
60
+ }
61
+ return { ...base, q: serializeFilterExpr(expr) };
62
+ }
63
+ function termsToFilterExpr(terms) {
64
+ return {
65
+ type: "and",
66
+ children: terms.map((t) => ({
67
+ type: "term",
68
+ key: t.key,
69
+ value: t.value
70
+ }))
71
+ };
72
+ }
73
+ export {
74
+ EMPTY_FILTER,
75
+ buildListParams,
76
+ collectTerms,
77
+ flattenFilterExpr,
78
+ isEmptyFilterExpr,
79
+ serializeFilterExpr,
80
+ termsToFilterExpr
81
+ };
82
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/search/types.ts","../../src/search/filterExpr.ts"],"sourcesContent":["/**\n * FilterExpr -- lightweight boolean expression tree for search and filtering.\n *\n * Designed to start simple (a flat AND list of terms) and grow to support\n * full boolean queries (AND, OR, NOT, grouping) without breaking callers.\n *\n * The component always emits a FilterExpr. Callers that talk to a flat\n * server API today use flattenFilterExpr() to extract the terms. When the\n * server gains AST support, callers drop flattenFilterExpr and pass the\n * expression directly -- no component changes required.\n *\n * Extractable to @petrarca/sonnet-core: no app-specific imports.\n */\n\n/** A single field/value filter term -- the leaf node of a FilterExpr. */\nexport interface FilterTerm {\n type: \"term\";\n /** Schema property key -- used as the query param name sent to the server. */\n key: string;\n /** The filter value. */\n value: string;\n}\n\n/** Conjunction: all children must match. */\nexport interface FilterAnd {\n type: \"and\";\n children: FilterExpr[];\n}\n\n/** Disjunction: at least one child must match. */\nexport interface FilterOr {\n type: \"or\";\n children: FilterExpr[];\n}\n\n/** Negation: child must not match. */\nexport interface FilterNot {\n type: \"not\";\n child: FilterExpr;\n}\n\n/**\n * Explicit grouping (parentheses). Semantically identical to its child --\n * used to preserve user intent when round-tripping the expression.\n */\nexport interface FilterGroup {\n type: \"group\";\n child: FilterExpr;\n}\n\n/** The full expression type. */\nexport type FilterExpr =\n | FilterTerm\n | FilterAnd\n | FilterOr\n | FilterNot\n | FilterGroup;\n\n/** An empty expression -- no filters active. */\nexport const EMPTY_FILTER: FilterExpr = { type: \"and\", children: [] };\n","/**\n * Utilities for working with FilterExpr trees.\n *\n * Extractable to @petrarca/sonnet-core: no app-specific imports.\n */\n\nimport type { FilterExpr, FilterTerm } from \"./types\";\n\n/**\n * Return all term nodes from the expression as FilterTerm objects, depth-first.\n *\n * Ignores the boolean structure (AND / OR / NOT / GROUP).\n */\nexport function collectTerms(expr: FilterExpr): FilterTerm[] {\n const out: FilterTerm[] = [];\n collectTermNodes(expr, out);\n return out;\n}\n\nfunction collectTermNodes(expr: FilterExpr, out: FilterTerm[]): void {\n switch (expr.type) {\n case \"term\":\n out.push(expr);\n break;\n case \"and\":\n case \"or\":\n for (const child of expr.children) collectTermNodes(child, out);\n break;\n case \"not\":\n case \"group\":\n collectTermNodes(expr.child, out);\n break;\n }\n}\n\n/**\n * Collect all term nodes from a FilterExpr tree as flat key/value pairs.\n *\n * Use this when talking to a server API that accepts only flat key/value\n * filter params. When the server gains AST support, pass the raw FilterExpr\n * instead and drop this call.\n *\n * @example\n * const terms = flattenFilterExpr(expr);\n * // [{ key: \"name\", value: \"acme\" }, { key: \"status\", value: \"active\" }]\n * const params = Object.fromEntries(terms.map(t => [t.key, t.value]));\n */\nexport function flattenFilterExpr(\n expr: FilterExpr,\n): { key: string; value: string }[] {\n return collectTerms(expr).map(({ key, value }) => ({ key, value }));\n}\n\n/**\n * Return true when the expression contains no term nodes.\n */\nexport function isEmptyFilterExpr(expr: FilterExpr): boolean {\n return collectTerms(expr).length === 0;\n}\n\n/**\n * Check whether a FilterExpr is a flat AND of terms only (no OR/NOT/Group).\n * When true, the expression can be serialized as simple key=value params.\n * When false, it must be serialized as a `q=` expression string.\n */\nfunction isFlatAnd(expr: FilterExpr): boolean {\n if (expr.type === \"term\") return true;\n if (expr.type === \"and\") return expr.children.every((c) => c.type === \"term\");\n return false;\n}\n\n/**\n * Serialize a FilterExpr to the server's q= expression syntax.\n *\n * Produces strings like `code ~ \"aspirin\" OR display ~ \"aspirin\"`.\n * All terms use the `~` (LIKE / substring) operator -- the FilterTerm\n * type carries no operator field; callers that need exact-match or other\n * operators should construct the q= string directly.\n */\nexport function serializeFilterExpr(expr: FilterExpr): string {\n switch (expr.type) {\n case \"term\": {\n const escaped = expr.value.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\n return `${expr.key} ~ \"${escaped}\"`;\n }\n case \"and\":\n return expr.children.map((c) => serializeFilterExpr(c)).join(\" AND \");\n case \"or\":\n return expr.children.map((c) => serializeFilterExpr(c)).join(\" OR \");\n case \"not\":\n return `NOT (${serializeFilterExpr(expr.child)})`;\n case \"group\":\n return `(${serializeFilterExpr(expr.child)})`;\n }\n}\n\n/**\n * Build a typed API params object from a FilterExpr and pagination state.\n *\n * Used by list and expansion pages to convert Zustand store state into\n * query params for the server. The base object carries pagination keys\n * (limit / offset); filter terms from the expression are merged in.\n *\n * When the expression is a flat AND of terms, each term becomes a flat\n * query param (backwards compatible with existing server endpoints).\n * When the expression contains OR/NOT/Group nodes, it is serialized\n * as a `q=` string using the server's query expression syntax.\n *\n * @example\n * const params = buildListParams<ValueSetListParams>(\n * filterExpr,\n * { limit: pagination.pageSize, offset: pagination.pageIndex * pagination.pageSize },\n * );\n */\nexport function buildListParams<T extends object>(\n expr: FilterExpr,\n base: T,\n): T {\n if (isEmptyFilterExpr(expr)) return base;\n\n // Flat AND of terms: serialize as individual key=value params.\n if (isFlatAnd(expr)) {\n const pairs = flattenFilterExpr(expr);\n const extra = Object.fromEntries(\n pairs\n .filter(({ key }) => !(key in base))\n .map(({ key, value }) => [key, value]),\n );\n return { ...base, ...extra } as T;\n }\n\n // Complex expression: serialize as q= string.\n return { ...base, q: serializeFilterExpr(expr) } as T;\n}\n\n/**\n * Build a flat AND expression from an array of key/value pairs.\n * Convenience for constructing the initial state from existing filter params.\n */\nexport function termsToFilterExpr(\n terms: { key: string; value: string }[],\n): FilterExpr {\n return {\n type: \"and\",\n children: terms.map((t) => ({\n type: \"term\",\n key: t.key,\n value: t.value,\n })),\n };\n}\n"],"mappings":";AA2DO,IAAM,eAA2B,EAAE,MAAM,OAAO,UAAU,CAAC,EAAE;;;AC9C7D,SAAS,aAAa,MAAgC;AAC3D,QAAM,MAAoB,CAAC;AAC3B,mBAAiB,MAAM,GAAG;AAC1B,SAAO;AACT;AAEA,SAAS,iBAAiB,MAAkB,KAAyB;AACnE,UAAQ,KAAK,MAAM;AAAA,IACjB,KAAK;AACH,UAAI,KAAK,IAAI;AACb;AAAA,IACF,KAAK;AAAA,IACL,KAAK;AACH,iBAAW,SAAS,KAAK,SAAU,kBAAiB,OAAO,GAAG;AAC9D;AAAA,IACF,KAAK;AAAA,IACL,KAAK;AACH,uBAAiB,KAAK,OAAO,GAAG;AAChC;AAAA,EACJ;AACF;AAcO,SAAS,kBACd,MACkC;AAClC,SAAO,aAAa,IAAI,EAAE,IAAI,CAAC,EAAE,KAAK,MAAM,OAAO,EAAE,KAAK,MAAM,EAAE;AACpE;AAKO,SAAS,kBAAkB,MAA2B;AAC3D,SAAO,aAAa,IAAI,EAAE,WAAW;AACvC;AAOA,SAAS,UAAU,MAA2B;AAC5C,MAAI,KAAK,SAAS,OAAQ,QAAO;AACjC,MAAI,KAAK,SAAS,MAAO,QAAO,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM;AAC5E,SAAO;AACT;AAUO,SAAS,oBAAoB,MAA0B;AAC5D,UAAQ,KAAK,MAAM;AAAA,IACjB,KAAK,QAAQ;AACX,YAAM,UAAU,KAAK,MAAM,QAAQ,OAAO,MAAM,EAAE,QAAQ,MAAM,KAAK;AACrE,aAAO,GAAG,KAAK,GAAG,OAAO,OAAO;AAAA,IAClC;AAAA,IACA,KAAK;AACH,aAAO,KAAK,SAAS,IAAI,CAAC,MAAM,oBAAoB,CAAC,CAAC,EAAE,KAAK,OAAO;AAAA,IACtE,KAAK;AACH,aAAO,KAAK,SAAS,IAAI,CAAC,MAAM,oBAAoB,CAAC,CAAC,EAAE,KAAK,MAAM;AAAA,IACrE,KAAK;AACH,aAAO,QAAQ,oBAAoB,KAAK,KAAK,CAAC;AAAA,IAChD,KAAK;AACH,aAAO,IAAI,oBAAoB,KAAK,KAAK,CAAC;AAAA,EAC9C;AACF;AAoBO,SAAS,gBACd,MACA,MACG;AACH,MAAI,kBAAkB,IAAI,EAAG,QAAO;AAGpC,MAAI,UAAU,IAAI,GAAG;AACnB,UAAM,QAAQ,kBAAkB,IAAI;AACpC,UAAM,QAAQ,OAAO;AAAA,MACnB,MACG,OAAO,CAAC,EAAE,IAAI,MAAM,EAAE,OAAO,KAAK,EAClC,IAAI,CAAC,EAAE,KAAK,MAAM,MAAM,CAAC,KAAK,KAAK,CAAC;AAAA,IACzC;AACA,WAAO,EAAE,GAAG,MAAM,GAAG,MAAM;AAAA,EAC7B;AAGA,SAAO,EAAE,GAAG,MAAM,GAAG,oBAAoB,IAAI,EAAE;AACjD;AAMO,SAAS,kBACd,OACY;AACZ,SAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAU,MAAM,IAAI,CAAC,OAAO;AAAA,MAC1B,MAAM;AAAA,MACN,KAAK,EAAE;AAAA,MACP,OAAO,EAAE;AAAA,IACX,EAAE;AAAA,EACJ;AACF;","names":[]}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Entity option fetcher contract.
3
+ *
4
+ * Defines the transport-agnostic interface for fetching selectable options
5
+ * from a backend data source (REST, GraphQL, or any async provider).
6
+ * Extractable to @petrarca/sonnet-core -- no app-specific imports.
7
+ */
8
+ /** A single selectable option. */
9
+ interface EntityOption {
10
+ value: string;
11
+ label: string;
12
+ }
13
+ /** Parameters passed to the fetcher on each request. */
14
+ interface EntityFetchParams {
15
+ /** Search string from the combobox input (debounced). */
16
+ search?: string;
17
+ /** Maximum number of options to return. */
18
+ limit?: number;
19
+ }
20
+ /**
21
+ * A function that fetches entity options from a data source.
22
+ *
23
+ * Implementations are transport-specific (REST, GraphQL, static, etc.)
24
+ * but all return the same shape. The hook and component never know
25
+ * which transport is used.
26
+ */
27
+ type EntityOptionFetcher = (params: EntityFetchParams) => Promise<EntityOption[]>;
28
+ /**
29
+ * Configuration for creating a REST-backed fetcher.
30
+ * Passed via x-ui-options when the data source is a REST list endpoint.
31
+ */
32
+ interface RestFetcherConfig {
33
+ /** REST endpoint path (e.g. "/organizations"). Relative to API base URL. */
34
+ endpoint: string;
35
+ /** Property key used as the option value (e.g. "org_id"). */
36
+ valueKey: string;
37
+ /** Property key used as the option label (e.g. "name"). */
38
+ labelKey: string;
39
+ /**
40
+ * Query parameter name for the search term (default: "search").
41
+ * Override when the endpoint uses a different filter key (e.g. "name").
42
+ */
43
+ searchKey?: string;
44
+ }
45
+ /**
46
+ * Configuration for creating a GraphQL-backed fetcher.
47
+ * Passed via x-ui-options when the data source is a GraphQL query.
48
+ */
49
+ interface GraphqlFetcherConfig {
50
+ /** GraphQL query document (a gql`` tagged template or raw string). */
51
+ query: string;
52
+ /** Dot path into the response data where the array lives (e.g. "organizations"). */
53
+ dataPath: string;
54
+ /** Property key used as the option value (e.g. "org_id"). */
55
+ valueKey: string;
56
+ /** Property key used as the option label (e.g. "name"). */
57
+ labelKey: string;
58
+ }
59
+ /**
60
+ * Factory function that creates an EntityOptionFetcher from widget options.
61
+ *
62
+ * The FetcherProvider supplies this factory via React context. The widget
63
+ * calls it with the x-ui-options to get a fetcher instance, without knowing
64
+ * which transport client is used.
65
+ */
66
+ type EntityFetcherFactory = (options: RestFetcherConfig | GraphqlFetcherConfig) => EntityOptionFetcher;
67
+
68
+ export type { EntityOptionFetcher as E, GraphqlFetcherConfig as G, RestFetcherConfig as R, EntityOption as a, EntityFetcherFactory as b, EntityFetchParams as c };
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@petrarca/sonnet-core",
3
+ "version": "0.1.0",
4
+ "description": "Shared utilities, schema tools, and hooks for the Petrarca Sonnet component library",
5
+ "license": "Apache-2.0",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "type": "module",
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "import": "./dist/index.js",
15
+ "types": "./dist/index.d.ts"
16
+ },
17
+ "./schema": {
18
+ "import": "./dist/schema/index.js",
19
+ "types": "./dist/schema/index.d.ts"
20
+ },
21
+ "./search": {
22
+ "import": "./dist/search/index.js",
23
+ "types": "./dist/search/index.d.ts"
24
+ },
25
+ "./hooks": {
26
+ "import": "./dist/hooks/index.js",
27
+ "types": "./dist/hooks/index.d.ts"
28
+ },
29
+ "./entityOptions": {
30
+ "import": "./dist/entityOptions/index.js",
31
+ "types": "./dist/entityOptions/index.d.ts"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist"
36
+ ],
37
+ "dependencies": {
38
+ "ajv": "^8.17.1",
39
+ "ajv-formats": "^3.0.1",
40
+ "class-variance-authority": "^0.7.1",
41
+ "clsx": "^2.1.1",
42
+ "nanoid": "^5.1.6",
43
+ "tailwind-merge": "3.3.1",
44
+ "zod": "^4.3.6"
45
+ },
46
+ "peerDependencies": {
47
+ "react": ">=18",
48
+ "@tanstack/react-query": ">=5"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "@tanstack/react-query": {
52
+ "optional": true
53
+ }
54
+ },
55
+ "devDependencies": {
56
+ "@types/react": "^19.0.0",
57
+ "tsup": "^8.4.0",
58
+ "typescript": "^5.9.2"
59
+ },
60
+ "scripts": {
61
+ "build": "tsup",
62
+ "dev": "tsup --watch",
63
+ "typecheck": "tsc --noEmit",
64
+ "lint": "tsc --noEmit",
65
+ "test": "vitest run",
66
+ "clean": "rm -rf dist *.tsbuildinfo"
67
+ }
68
+ }