n8n-nodes-lifespace 0.1.5 → 0.1.7

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/README.md CHANGED
@@ -70,13 +70,23 @@ Create/Update keep scalar fields in n8n Resource Mapper while using native n8n c
70
70
 
71
71
  The node displays the authorized human-readable `spaceName` when present while continuing to submit the stable `spc_*` ID.
72
72
 
73
- Record Type identity and REST routing remain distinct. LifeSpace `modelKey` is the stable semantic identity, while the Discovery `route` is a REST transport detail. The n8n adapter persists an adapter-local `recordType` selector containing both values when the workflow is configured. At execution time the selector is decoded locally, so Get/List/Delete do not add a Discovery request merely to translate `modelKey` to a REST route.
73
+ Record Type is the LifeSpace `modelKey` (for example `task`). Design-time options, expressions, Trigger output and downstream Record nodes all use that plain value. CRUD calls go directly to `/spaces/{spaceId}/models/{modelKey}/records/...`, so execution adds no Discovery request and the adapter maintains no modelKey-to-route mapping. Existing `lsrt1...` workflow values are decoded only as a deprecated read-compatibility path and are never emitted or written by new configuration.
74
74
 
75
75
  Calendar-backed models use canonical `capabilityBindings.calendar` field roles instead of Event-specific field names while the node is being configured. Date-only values are normalized to `YYYY-MM-DD`. Create/Update execution does not fetch fresh semantic Discovery solely to produce an adapter-local Calendar conflict error; the canonical mutation goes directly to LifeSpace Core, which remains authoritative for Calendar validation and current authorization.
76
76
 
77
+ ## AI Agent Tool
78
+
79
+ `LifeSpace Tool` is the metadata-driven AI Agent sub-node. Configure one fixed Space, Record Type and semantic operation per Tool instance, then connect multiple instances to the same n8n AI Agent. The Tool name, default description and structured AI input schema are generated from LifeSpace Progressive Runtime Discovery; hand-written descriptions are optional.
80
+
81
+ - Query exposes published search/filter/comparison/local-date-window/sort/pagination semantics. Capability Query mode exposes only the capability-owned parameters and ordering that LifeSpace explicitly publishes; generic-facet composition stays narrowed until LifeSpace #228 defines it.
82
+ - Create/Update schemas come from writable Model fields. Required fields with LifeSpace defaults are optional AI inputs, and fields the AI omits are absent from the outgoing request rather than synthesized as empty/null placeholders.
83
+ - Delete and Action hide optimistic-concurrency `version` from the AI. The Adapter reads the current record version when required and Core remains the final authorization/concurrency/semantic authority.
84
+ - No Task/Event/Wish-specific Tool nodes or field maps are shipped. New models become available through Discovery without source changes.
85
+ - The ordinary `LifeSpace` workflow node remains `usableAsTool` for manually configured `$fromAI(...)` workflows, but `LifeSpace Tool` is the canonical path when the AI should receive the complete dynamic LifeSpace schema.
86
+
77
87
  ## LifeSpace contract compatibility
78
88
 
79
- This package follows the current LifeSpace Core Kernel `0.31.0` contract family. It consumes Runtime/Discovery semantics only; Eventing configuration and webhook delivery semantics are owned independently by Integration/Eventing `0.1.0`.
89
+ This package follows the current LifeSpace Core Kernel `0.35.0` contract family. It consumes Runtime/Discovery semantics only; Eventing configuration and webhook delivery semantics are owned independently by Integration/Eventing `0.1.0`.
80
90
 
81
91
  The UX depends on these Kernel capabilities:
82
92
 
@@ -94,6 +104,10 @@ The UX depends on these Kernel capabilities:
94
104
  - `0.29.0`: canonical ordinary-record `referenceLabel` semantics plus `record` / `record_list` lookup and resolution;
95
105
  - `0.30.0`: explicit paginated Change History collection and Model Control Plane ownership split;
96
106
  - `0.31.0`: Integration/Eventing wire representation moves to the independent Integration/Eventing `0.1.0` contract while Core remains the Runtime authority.
107
+ - `0.32.0`: `modelKey` becomes the sole Runtime address and canonical CRUD/Action paths move under `/models/{modelKey}/records`.
108
+ - `0.33.0`: canonical structural `timeRanges` become available in progressive semantic detail without implying an overlap query API.
109
+ - `0.34.0`: explicit `eq/lt/lte/gt/gte` comparison transports, first-class `createdAt` / `updatedAt` envelope comparisons and Core-owned datetime local-date-window conversion become discoverable.
110
+ - `0.35.0`: grouped `query.capabilityQueries` adds the preferred `calendar.window` viewing-window query with explicit IANA viewing timezone and deterministic mixed all-day/timed ordering.
97
111
 
98
112
  The adapter prefers the `0.27+` progressive flow while configuring a node:
99
113
 
@@ -105,9 +119,9 @@ GET /me/_discovery/inventory
105
119
  -> persist stable workflow configuration
106
120
  ```
107
121
 
108
- Design-time callbacks prefer n8n editor-current parameters, so Fields and Actions refresh immediately after Record Type selection. A legacy 0.1.3 `modelRoute` is still readable for design-time compatibility; re-selecting Record Type moves configuration to the composable selector.
122
+ Design-time callbacks prefer n8n editor-current parameters, so Fields and Actions refresh immediately after Record Type selection. A legacy 0.1.3 `modelRoute` remains readable only for the four previously deployed baseline models; re-selecting Record Type writes the plain `modelKey`.
109
123
 
110
- Execution is deliberately narrower. Get/List/Delete derive the REST route locally from the persisted Record Type selector. Create/Update submit the configured mutation without a fresh Runtime Discovery preflight. Execute Action loads only the selected model's `0.26+` static semantic detail, and reuses that detail within the same node execution for repeated items using the same Space/Record Type. No cached Discovery result is treated as authorization proof; every CRUD or Action request still goes through canonical LifeSpace Core current-state enforcement.
124
+ Execution is deliberately narrower. Get/List/Create/Update/Delete call the canonical modelKey-addressed Runtime path directly without a fresh Runtime Discovery preflight. Execute Action loads only the selected model's `0.26+` static semantic detail, and reuses that detail within the same node execution for repeated items using the same Space/Record Type. No cached Discovery result is treated as authorization proof; every CRUD or Action request still goes through canonical LifeSpace Core current-state enforcement.
111
125
 
112
126
  The legacy aggregate `GET /me/_discovery` remains an intentional design-time compatibility fallback. Neither cached Discovery nor relation lookup is treated as authorization proof; every mutation still goes through canonical LifeSpace Runtime enforcement.
113
127
 
@@ -126,7 +140,7 @@ Examples:
126
140
  {{$vars.lifeSpaceRecordType}}
127
141
  ```
128
142
 
129
- Discovery-backed selectors such as **Space**, **Filter Field**, **Sort Field** and **Action** support the normal n8n pattern: choose a value from the list, or switch the parameter to an expression and provide the corresponding stable ID/key. **Record Type** uses the adapter-local `recordType` selector so a LifeSpace Trigger can feed a Record node directly without a mapping step or an extra Discovery request.
143
+ Discovery-backed selectors such as **Space**, **Filter Field**, **Sort Field** and **Action** support the normal n8n pattern: choose a value from the list, or switch the parameter to an expression and provide the corresponding stable ID/key. **Record Type** stores the plain LifeSpace `modelKey`, so a LifeSpace Trigger can feed a Record node directly without a mapping step or an extra Discovery request.
130
144
 
131
145
  The same applies to ordinary values such as Record ID, Search, Filter Value, Return All, Limit, Sort Direction, Cursor, explicit Version, API Method, API Path and JSON Body.
132
146
 
@@ -198,9 +212,14 @@ The normal UI supports:
198
212
 
199
213
  - optional **Search**;
200
214
  - one or more typed **Filters**;
215
+ - Discovery-driven explicit **Number Comparison** and **Date / Time Comparison** rows using the exact LifeSpace-published operator transport;
216
+ - generic **Local Date Windows** for datetime fields advertised by Time Semantics, including envelope `createdAt` / `updatedAt`, with local dates + IANA timezone sent unchanged to Core;
217
+ - optional grouped **Semantic Query** input generated from `query.capabilityQueries` (for example `calendar.window`) plus its published semantic ordering;
201
218
  - **Return All** to follow `nextCursor` automatically;
202
219
  - **Limit** when Return All is disabled.
203
220
 
221
+ The adapter never derives explicit comparison parameter names from field naming and never converts local calendar windows to UTC. Those transport names and timezone/DST semantics come from LifeSpace Runtime Semantic Detail. Existing `exact/from/to` filters remain available as compatibility UI and preserve the legacy inclusive `To` behavior.
222
+
204
223
  Use **Sorts → Add Sort** to add zero or more sort criteria in priority order. Sortable model fields use authoritative `title` metadata from Runtime Discovery, while envelope fields such as `createdAt` / `updatedAt` are offered only when Discovery advertises them. Multiple criteria are sent as ordered repeated `sort=field:direction` query parameters.
205
224
 
206
225
  Advanced **Options** contain manual **Cursor** as an escape hatch.
@@ -256,7 +275,7 @@ One Webhook Endpoint can therefore carry events for multiple Record Types throug
256
275
 
257
276
  The Trigger verifies `X-LifeSpace-Timestamp` and `X-LifeSpace-Signature` with the LifeSpace HMAC-SHA256 contract before emitting workflow data. `endpoint.test` payloads are always accepted after signature verification.
258
277
 
259
- For ordinary record events, the Trigger preserves the LifeSpace `modelKey` exactly as delivered and additionally emits the n8n adapter-local `recordType` selector for the matching configured Record Type. Pass `{{$json.recordType}}` and `{{$json.recordId}}` directly to a downstream LifeSpace Record node. The downstream node derives the REST route locally from that selector.
278
+ For ordinary record events, the Trigger preserves the LifeSpace `modelKey` exactly as delivered and emits the same plain value as `recordType` for n8n composition. Pass `{{$json.recordType}}` and `{{$json.recordId}}` directly to a downstream LifeSpace Record node; it calls the canonical modelKey-addressed Runtime path with zero Discovery.
260
279
 
261
280
  Webhook Endpoint / Event Subscription creation is intentionally not performed by the n8n Service API Token today because LifeSpace treats that configuration as a Space-management operation. The adapter does not bypass that authority boundary.
262
281
 
@@ -318,4 +337,4 @@ The adapter now consumes generic `person`, `person_list`, `record` and `record_l
318
337
 
319
338
  ## License
320
339
 
321
- MIT
340
+ MIT
@@ -16,6 +16,12 @@ export declare class LifeSpace implements INodeType {
16
16
  getBooleanFilterableFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
17
17
  getNumericFilterableFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
18
18
  getTemporalFilterableFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
19
+ getNumericComparisonFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
20
+ getTemporalComparisonFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
21
+ getComparisonOperatorsForCurrentField(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
22
+ getLocalDateWindowFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
23
+ getCapabilityQueries(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
24
+ getSemanticSorts(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
19
25
  getPersonFilterableFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
20
26
  getEnumValuesForCurrentFilter(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
21
27
  getSortableFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
@@ -23,6 +29,7 @@ export declare class LifeSpace implements INodeType {
23
29
  resourceMapping: {
24
30
  getRecordFields(this: ILoadOptionsFunctions): Promise<ResourceMapperFields>;
25
31
  getActionInputFields(this: ILoadOptionsFunctions): Promise<ResourceMapperFields>;
32
+ getSemanticQueryInputFields(this: ILoadOptionsFunctions): Promise<ResourceMapperFields>;
26
33
  };
27
34
  };
28
35
  execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
@@ -3,6 +3,81 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LifeSpace = void 0;
4
4
  const n8n_workflow_1 = require("n8n-workflow");
5
5
  const lifespaceDiscovery_1 = require("../lifespaceDiscovery");
6
+ const LEGACY_MODEL_ROUTE_TO_KEY = Object.freeze({
7
+ tasks: 'task',
8
+ wishes: 'wish',
9
+ 'day-records': 'day_record',
10
+ events: 'event',
11
+ });
12
+ const LOCAL_DATE_WINDOW_SELECTOR_PREFIX = 'lsqw1.';
13
+ const COMPARISON_OPERATOR_LABELS = Object.freeze({
14
+ eq: 'Equals',
15
+ lt: 'Less Than',
16
+ lte: 'Less Than or Equal',
17
+ gt: 'Greater Than',
18
+ gte: 'Greater Than or Equal',
19
+ });
20
+ function comparisonFieldParts(value) {
21
+ const raw = String(value !== null && value !== void 0 ? value : '').trim();
22
+ const separator = raw.indexOf(':');
23
+ if (separator <= 0)
24
+ return { field: raw, valueType: '' };
25
+ return { valueType: raw.slice(0, separator), field: raw.slice(separator + 1) };
26
+ }
27
+ function encodeLocalDateWindowSelector(comparison) {
28
+ const window = comparison.localDateWindow;
29
+ if (!window || comparison.valueType !== 'datetime')
30
+ return '';
31
+ return `${LOCAL_DATE_WINDOW_SELECTOR_PREFIX}${Buffer.from(JSON.stringify([
32
+ comparison.field,
33
+ comparison.valueType,
34
+ window.dateStartParameter,
35
+ window.dateEndExclusiveParameter,
36
+ window.timezoneParameter,
37
+ ])).toString('base64url')}`;
38
+ }
39
+ function decodeLocalDateWindowSelector(value) {
40
+ const raw = String(value !== null && value !== void 0 ? value : '').trim();
41
+ if (!raw.startsWith(LOCAL_DATE_WINDOW_SELECTOR_PREFIX))
42
+ return null;
43
+ try {
44
+ const decoded = JSON.parse(Buffer.from(raw.slice(LOCAL_DATE_WINDOW_SELECTOR_PREFIX.length), 'base64url').toString('utf8'));
45
+ if (!Array.isArray(decoded) || decoded.length !== 5 || decoded.some((entry) => typeof entry !== 'string' || !entry))
46
+ return null;
47
+ if (decoded[1] !== 'datetime')
48
+ return null;
49
+ return {
50
+ field: decoded[0],
51
+ valueType: 'datetime',
52
+ dateStartParameter: decoded[2],
53
+ dateEndExclusiveParameter: decoded[3],
54
+ timezoneParameter: decoded[4],
55
+ };
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }
61
+ function semanticQueryMapperType(parameter) {
62
+ if (parameter.type === 'boolean')
63
+ return 'boolean';
64
+ if (parameter.type === 'integer' || parameter.type === 'number')
65
+ return 'number';
66
+ return 'string';
67
+ }
68
+ function semanticQueryMapperField(parameter) {
69
+ var _a;
70
+ return {
71
+ id: parameter.parameter,
72
+ displayName: (0, lifespaceDiscovery_1.humanizeKey)(((_a = parameter.role) === null || _a === void 0 ? void 0 : _a.trim()) || parameter.parameter),
73
+ required: parameter.required === true,
74
+ defaultMatch: false,
75
+ canBeUsedToMatch: false,
76
+ display: true,
77
+ type: semanticQueryMapperType(parameter),
78
+ description: `${parameter.type}${parameter.role ? ` · ${parameter.role}` : ''} · LifeSpace query parameter ${parameter.parameter}`,
79
+ };
80
+ }
6
81
  function parseJsonObject(context, itemIndex, value, fieldName) {
7
82
  const parsed = typeof value === 'string' ? JSON.parse(value) : value;
8
83
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
@@ -96,14 +171,15 @@ function normalizeActionInput(context, itemIndex, value, fields) {
96
171
  return result;
97
172
  }
98
173
  function queryParameters(context, itemIndex, limit, cursorOverride) {
99
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
174
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5;
100
175
  const qs = {};
101
176
  const search = String(context.getNodeParameter('search', itemIndex, '')).trim();
102
177
  const options = context.getNodeParameter('options', itemIndex, {});
103
178
  const configuredSorts = context.getNodeParameter('sorts.sort', itemIndex, []);
104
- const legacySortField = String((_a = options.sortField) !== null && _a !== void 0 ? _a : '').trim();
105
- const legacySortDirection = String((_b = options.sortDirection) !== null && _b !== void 0 ? _b : 'desc').trim();
106
- const configuredCursor = String((_c = options.cursor) !== null && _c !== void 0 ? _c : '').trim();
179
+ const semanticSort = String((_a = context.getNodeParameter('semanticSort', itemIndex, '')) !== null && _a !== void 0 ? _a : '').trim();
180
+ const legacySortField = String((_b = options.sortField) !== null && _b !== void 0 ? _b : '').trim();
181
+ const legacySortDirection = String((_c = options.sortDirection) !== null && _c !== void 0 ? _c : 'desc').trim();
182
+ const configuredCursor = String((_d = options.cursor) !== null && _d !== void 0 ? _d : '').trim();
107
183
  const cursor = cursorOverride !== null && cursorOverride !== void 0 ? cursorOverride : configuredCursor;
108
184
  const filters = context.getNodeParameter('filters.filter', itemIndex, []);
109
185
  if (search)
@@ -111,10 +187,10 @@ function queryParameters(context, itemIndex, limit, cursorOverride) {
111
187
  const orderedSorts = [];
112
188
  const usedSortFields = new Set();
113
189
  for (const sort of configuredSorts) {
114
- const field = String((_d = sort.field) !== null && _d !== void 0 ? _d : '').trim();
190
+ const field = String((_e = sort.field) !== null && _e !== void 0 ? _e : '').trim();
115
191
  if (!field)
116
192
  continue;
117
- const direction = String((_e = sort.direction) !== null && _e !== void 0 ? _e : 'asc').trim();
193
+ const direction = String((_f = sort.direction) !== null && _f !== void 0 ? _f : 'asc').trim();
118
194
  if (direction !== 'asc' && direction !== 'desc') {
119
195
  throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Sort direction for ${field} must be asc or desc`, { itemIndex });
120
196
  }
@@ -124,56 +200,64 @@ function queryParameters(context, itemIndex, limit, cursorOverride) {
124
200
  usedSortFields.add(field);
125
201
  orderedSorts.push(`${field}:${direction}`);
126
202
  }
127
- if (orderedSorts.length === 1)
203
+ if (semanticSort && (orderedSorts.length > 0 || legacySortField)) {
204
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'Semantic Sort cannot be combined with field Sorts', { itemIndex });
205
+ }
206
+ if (semanticSort)
207
+ qs.sort = semanticSort;
208
+ if (!semanticSort && orderedSorts.length === 1)
128
209
  qs.sort = orderedSorts[0];
129
- if (orderedSorts.length > 1)
210
+ if (!semanticSort && orderedSorts.length > 1)
130
211
  qs.sort = orderedSorts;
131
- if (orderedSorts.length === 0 && legacySortField)
212
+ if (!semanticSort && orderedSorts.length === 0 && legacySortField)
132
213
  qs.sort = `${legacySortField}:${legacySortDirection}`;
133
214
  qs.limit = limit;
134
215
  if (cursor)
135
216
  qs.cursor = cursor;
136
217
  const usedKeys = new Set();
137
- const setFilter = (field, operator, value) => {
138
- const key = operator === 'from' ? `${field}From` : operator === 'to' ? `${field}To` : field;
218
+ const setQueryParameter = (key, value) => {
139
219
  if (usedKeys.has(key)) {
140
- throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Query filter ${key} may be supplied only once`, { itemIndex });
220
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Query parameter ${key} may be supplied only once`, { itemIndex });
141
221
  }
142
222
  usedKeys.add(key);
143
223
  qs[key] = typeof value === 'boolean' ? String(value) : value;
144
224
  };
225
+ const setFilter = (field, operator, value) => {
226
+ const key = operator === 'from' ? `${field}From` : operator === 'to' ? `${field}To` : field;
227
+ setQueryParameter(key, value);
228
+ };
145
229
  for (const filter of filters) {
146
- const field = String((_f = filter.field) !== null && _f !== void 0 ? _f : '').trim();
147
- const value = String((_g = filter.value) !== null && _g !== void 0 ? _g : '');
148
- const operator = (_h = filter.operator) !== null && _h !== void 0 ? _h : 'exact';
230
+ const field = String((_g = filter.field) !== null && _g !== void 0 ? _g : '').trim();
231
+ const value = String((_h = filter.value) !== null && _h !== void 0 ? _h : '');
232
+ const operator = (_j = filter.operator) !== null && _j !== void 0 ? _j : 'exact';
149
233
  if (field && value !== '')
150
234
  setFilter(field, operator, value);
151
235
  }
152
236
  const typedFilters = context.getNodeParameter('filters', itemIndex, {});
153
- for (const row of ((_j = typedFilters.text) !== null && _j !== void 0 ? _j : [])) {
154
- const field = String((_k = row.field) !== null && _k !== void 0 ? _k : '').trim();
237
+ for (const row of ((_k = typedFilters.text) !== null && _k !== void 0 ? _k : [])) {
238
+ const field = String((_l = row.field) !== null && _l !== void 0 ? _l : '').trim();
155
239
  if (field && row.value !== undefined && row.value !== '')
156
240
  setFilter(field, 'exact', String(row.value));
157
241
  }
158
- for (const row of ((_l = typedFilters.enum) !== null && _l !== void 0 ? _l : [])) {
159
- const field = String((_m = row.field) !== null && _m !== void 0 ? _m : '').trim();
242
+ for (const row of ((_m = typedFilters.enum) !== null && _m !== void 0 ? _m : [])) {
243
+ const field = String((_o = row.field) !== null && _o !== void 0 ? _o : '').trim();
160
244
  const values = Array.isArray(row.values) ? row.values.map(String).filter(Boolean) : [];
161
245
  if (field && values.length)
162
246
  setFilter(field, 'exact', values.join(','));
163
247
  }
164
- for (const row of ((_o = typedFilters.boolean) !== null && _o !== void 0 ? _o : [])) {
165
- const field = String((_p = row.field) !== null && _p !== void 0 ? _p : '').trim();
248
+ for (const row of ((_p = typedFilters.boolean) !== null && _p !== void 0 ? _p : [])) {
249
+ const field = String((_q = row.field) !== null && _q !== void 0 ? _q : '').trim();
166
250
  if (field)
167
251
  setFilter(field, 'exact', Boolean(row.value));
168
252
  }
169
- for (const row of ((_q = typedFilters.number) !== null && _q !== void 0 ? _q : [])) {
170
- const field = String((_r = row.field) !== null && _r !== void 0 ? _r : '').trim();
253
+ for (const row of ((_r = typedFilters.number) !== null && _r !== void 0 ? _r : [])) {
254
+ const field = String((_s = row.field) !== null && _s !== void 0 ? _s : '').trim();
171
255
  const value = Number(row.value);
172
256
  if (field && Number.isFinite(value))
173
- setFilter(field, (_s = row.operator) !== null && _s !== void 0 ? _s : 'exact', value);
257
+ setFilter(field, (_t = row.operator) !== null && _t !== void 0 ? _t : 'exact', value);
174
258
  }
175
- for (const row of ((_t = typedFilters.temporal) !== null && _t !== void 0 ? _t : [])) {
176
- const encoded = String((_u = row.field) !== null && _u !== void 0 ? _u : '').trim();
259
+ for (const row of ((_u = typedFilters.temporal) !== null && _u !== void 0 ? _u : [])) {
260
+ const encoded = String((_v = row.field) !== null && _v !== void 0 ? _v : '').trim();
177
261
  if (!encoded || row.value === undefined || row.value === '')
178
262
  continue;
179
263
  const separator = encoded.indexOf(':');
@@ -182,14 +266,59 @@ function queryParameters(context, itemIndex, limit, cursorOverride) {
182
266
  const raw = String(row.value);
183
267
  const value = fieldType === 'date' ? dateOnlyValue(context, itemIndex, raw, field) : raw;
184
268
  if (value !== null)
185
- setFilter(field, (_v = row.operator) !== null && _v !== void 0 ? _v : 'exact', value);
269
+ setFilter(field, (_w = row.operator) !== null && _w !== void 0 ? _w : 'exact', value);
186
270
  }
187
- for (const row of ((_w = typedFilters.person) !== null && _w !== void 0 ? _w : [])) {
188
- const field = String((_x = row.field) !== null && _x !== void 0 ? _x : '').trim();
189
- const target = String((_y = row.target) !== null && _y !== void 0 ? _y : '').trim();
271
+ for (const row of ((_x = typedFilters.person) !== null && _x !== void 0 ? _x : [])) {
272
+ const field = String((_y = row.field) !== null && _y !== void 0 ? _y : '').trim();
273
+ const target = String((_z = row.target) !== null && _z !== void 0 ? _z : '').trim();
190
274
  if (field && target)
191
275
  setFilter(field, 'exact', target);
192
276
  }
277
+ for (const row of ((_0 = typedFilters.numberComparison) !== null && _0 !== void 0 ? _0 : [])) {
278
+ const field = comparisonFieldParts(row.field).field;
279
+ const parameter = String((_1 = row.parameter) !== null && _1 !== void 0 ? _1 : '').trim();
280
+ const value = Number(row.value);
281
+ if (field && parameter && Number.isFinite(value))
282
+ setQueryParameter(parameter, value);
283
+ }
284
+ for (const row of ((_2 = typedFilters.temporalComparison) !== null && _2 !== void 0 ? _2 : [])) {
285
+ const { field, valueType } = comparisonFieldParts(row.field);
286
+ const parameter = String((_3 = row.parameter) !== null && _3 !== void 0 ? _3 : '').trim();
287
+ if (!field || !parameter || row.value === undefined || row.value === '')
288
+ continue;
289
+ const raw = String(row.value);
290
+ const value = valueType === 'date' ? dateOnlyValue(context, itemIndex, raw, field) : raw;
291
+ if (value !== null)
292
+ setQueryParameter(parameter, value);
293
+ }
294
+ const localDateWindows = context.getNodeParameter('localDateWindows.window', itemIndex, []);
295
+ for (const row of localDateWindows) {
296
+ const selector = decodeLocalDateWindowSelector(row.field);
297
+ if (!selector) {
298
+ if (String((_4 = row.field) !== null && _4 !== void 0 ? _4 : '').trim()) {
299
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'Local Date Window selector is invalid. Re-select the field from Runtime Discovery.', { itemIndex });
300
+ }
301
+ continue;
302
+ }
303
+ const dateStart = dateOnlyValue(context, itemIndex, row.dateStart, selector.field);
304
+ const dateEndExclusive = dateOnlyValue(context, itemIndex, row.dateEndExclusive, selector.field);
305
+ const timezone = String((_5 = row.timezone) !== null && _5 !== void 0 ? _5 : '').trim();
306
+ if (dateStart !== null)
307
+ setQueryParameter(selector.dateStartParameter, dateStart);
308
+ if (dateEndExclusive !== null)
309
+ setQueryParameter(selector.dateEndExclusiveParameter, dateEndExclusive);
310
+ if (timezone)
311
+ setQueryParameter(selector.timezoneParameter, timezone);
312
+ }
313
+ const semanticQueryInput = mappedValue(context, itemIndex, 'semanticQueryInput');
314
+ for (const [parameter, value] of Object.entries(semanticQueryInput)) {
315
+ if (value === null || value === undefined || value === '')
316
+ continue;
317
+ if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
318
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Semantic Query parameter ${parameter} must be a scalar value`, { itemIndex });
319
+ }
320
+ setQueryParameter(parameter, value);
321
+ }
193
322
  return qs;
194
323
  }
195
324
  function queryPage(context, itemIndex, response) {
@@ -313,6 +442,7 @@ function actionOption(action) {
313
442
  };
314
443
  }
315
444
  async function optionModel(context) {
445
+ var _a, _b;
316
446
  const spaceId = (0, lifespaceDiscovery_1.loadOptionParameter)(context, 'spaceId');
317
447
  const recordTypeValue = (0, lifespaceDiscovery_1.loadOptionParameter)(context, 'recordType');
318
448
  const legacyModelRoute = recordTypeValue ? '' : (0, lifespaceDiscovery_1.loadOptionParameter)(context, 'modelRoute');
@@ -323,10 +453,7 @@ async function optionModel(context) {
323
453
  throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'LifeSpace Record Type selector is invalid. Choose a Record Type from Discovery or pass a Trigger recordType value.');
324
454
  }
325
455
  const discovery = await lifespaceDiscovery_1.loadRuntimeDiscovery.call(context);
326
- const space = (0, lifespaceDiscovery_1.discoverySpace)(discovery, spaceId);
327
- const model = recordType
328
- ? (0, lifespaceDiscovery_1.discoveryModel)(discovery, spaceId, recordType.modelKey)
329
- : space === null || space === void 0 ? void 0 : space.models.find((entry) => entry.route === legacyModelRoute);
456
+ const model = (0, lifespaceDiscovery_1.discoveryModel)(discovery, spaceId, (_b = (_a = recordType === null || recordType === void 0 ? void 0 : recordType.modelKey) !== null && _a !== void 0 ? _a : LEGACY_MODEL_ROUTE_TO_KEY[legacyModelRoute]) !== null && _b !== void 0 ? _b : '');
330
457
  return model ? { model, spaceId } : null;
331
458
  }
332
459
  async function filterFieldOptions(context, types, encodeType = false) {
@@ -345,6 +472,24 @@ async function filterFieldOptions(context, types, encodeType = false) {
345
472
  });
346
473
  });
347
474
  }
475
+ async function comparisonFieldOptions(context, types) {
476
+ var _a;
477
+ const selected = await optionModel(context);
478
+ if (!selected)
479
+ return [];
480
+ const allowed = new Set(types);
481
+ return ((_a = selected.model.query.comparisons) !== null && _a !== void 0 ? _a : [])
482
+ .filter((comparison) => allowed.has(comparison.valueType))
483
+ .map((comparison) => {
484
+ var _a;
485
+ const field = selected.model.fields.find((entry) => entry.key === comparison.field);
486
+ return {
487
+ name: ((_a = field === null || field === void 0 ? void 0 : field.title) === null || _a === void 0 ? void 0 : _a.trim()) || (0, lifespaceDiscovery_1.humanizeKey)(comparison.field),
488
+ value: `${comparison.valueType}:${comparison.field}`,
489
+ description: `${comparison.source} · ${comparison.valueType} · explicit LifeSpace comparison operators`,
490
+ };
491
+ });
492
+ }
348
493
  async function relationFieldOptions(context, cardinality, filterableOnly = false) {
349
494
  const selected = await optionModel(context);
350
495
  if (!selected)
@@ -533,6 +678,16 @@ class LifeSpace {
533
678
  { displayName: 'Operator', name: 'operator', type: 'options', options: [{ name: 'Equals', value: 'exact' }, { name: 'From / Greater Than or Equal', value: 'from' }, { name: 'To / Less Than or Equal', value: 'to' }], default: 'exact' },
534
679
  { displayName: 'Value', name: 'value', type: 'dateTime', default: '', required: true },
535
680
  ] },
681
+ { displayName: 'Number Comparison', name: 'numberComparison', values: [
682
+ { displayName: 'Field Name or ID', name: 'field', type: 'options', description: 'Fields and envelope values advertised by LifeSpace explicit comparison semantics. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.', typeOptions: { loadOptionsMethod: 'getNumericComparisonFields', loadOptionsDependsOn: ['spaceId', 'recordType'] }, options: [], default: '', required: true },
683
+ { displayName: 'Operator Name or ID', name: 'parameter', type: 'options', description: 'The option value is the exact query parameter published by LifeSpace; the adapter does not derive transport names. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.', typeOptions: { loadOptionsMethod: 'getComparisonOperatorsForCurrentField', loadOptionsDependsOn: ['spaceId', 'recordType', '&field'] }, options: [], default: '', required: true },
684
+ { displayName: 'Value', name: 'value', type: 'number', default: 0, required: true },
685
+ ] },
686
+ { displayName: 'Date / Time Comparison', name: 'temporalComparison', values: [
687
+ { displayName: 'Field Name or ID', name: 'field', type: 'options', description: 'Includes model date/datetime fields plus LifeSpace envelope timestamps such as createdAt/updatedAt when advertised. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.', typeOptions: { loadOptionsMethod: 'getTemporalComparisonFields', loadOptionsDependsOn: ['spaceId', 'recordType'] }, options: [], default: '', required: true },
688
+ { displayName: 'Operator Name or ID', name: 'parameter', type: 'options', description: 'Uses the exact explicit comparison transport advertised by LifeSpace. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.', typeOptions: { loadOptionsMethod: 'getComparisonOperatorsForCurrentField', loadOptionsDependsOn: ['spaceId', 'recordType', '&field'] }, options: [], default: '', required: true },
689
+ { displayName: 'Value', name: 'value', type: 'dateTime', default: '', required: true },
690
+ ] },
536
691
  { displayName: 'Person Filter', name: 'person', values: [
537
692
  { displayName: 'Field Name or ID', name: 'field', type: 'options', description: 'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>', typeOptions: { loadOptionsMethod: 'getPersonFilterableFields', loadOptionsDependsOn: ['spaceId', 'recordType'] }, options: [], default: '', required: true },
538
693
  { displayName: 'Person Name or ID', name: 'target', type: 'options', description: 'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>', typeOptions: { loadOptionsMethod: 'getRelationTargetsForCurrentField', loadOptionsDependsOn: ['spaceId', 'recordType', '&field'] }, options: [], default: '', required: true },
@@ -544,6 +699,45 @@ class LifeSpace {
544
699
  ] },
545
700
  ],
546
701
  },
702
+ {
703
+ displayName: 'Local Date Windows', name: 'localDateWindows', type: 'fixedCollection', default: {},
704
+ placeholder: 'Add Local Date Window', typeOptions: { multipleValues: true },
705
+ displayOptions: { show: { resource: ['modelRecord'], operation: ['list'] } },
706
+ options: [{ displayName: 'Window', name: 'window', values: [
707
+ { displayName: 'Field Name or ID', name: 'field', type: 'options', typeOptions: { loadOptionsMethod: 'getLocalDateWindowFields', loadOptionsDependsOn: ['spaceId', 'recordType'] }, options: [], default: '', required: true, description: 'Only datetime fields whose published comparison semantics include a local-date-window transport are offered. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.' },
708
+ { displayName: 'Start Date', name: 'dateStart', type: 'dateTime', default: '', required: true, description: 'Inclusive local calendar start date. The adapter submits YYYY-MM-DD and does not calculate UTC boundaries.' },
709
+ { displayName: 'End Date (Exclusive)', name: 'dateEndExclusive', type: 'dateTime', default: '', required: true, description: 'Exclusive local calendar end date' },
710
+ { displayName: 'Viewing Timezone', name: 'timezone', type: 'string', default: '', required: true, placeholder: 'Europe/Amsterdam', description: 'IANA timezone passed unchanged to LifeSpace Core' },
711
+ ] }],
712
+ },
713
+ {
714
+ displayName: 'Semantic Query Name or ID', name: 'semanticQueryKey', type: 'options',
715
+ typeOptions: { loadOptionsMethod: 'getCapabilityQueries', loadOptionsDependsOn: ['spaceId', 'recordType'] },
716
+ options: [], default: '',
717
+ displayOptions: { show: { resource: ['modelRecord'], operation: ['list'] } },
718
+ description: 'Optional grouped capability query published by LifeSpace, for example Calendar Window. The adapter does not hard-code Event fields. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
719
+ },
720
+ {
721
+ displayName: 'Semantic Query Input', name: 'semanticQueryInput', type: 'resourceMapper',
722
+ default: { mappingMode: 'defineBelow', value: null }, noDataExpression: true,
723
+ typeOptions: {
724
+ loadOptionsDependsOn: ['spaceId', 'recordType', 'semanticQueryKey'],
725
+ resourceMapper: {
726
+ resourceMapperMethod: 'getSemanticQueryInputFields', mode: 'add',
727
+ fieldWords: { singular: 'parameter', plural: 'parameters' }, addAllFields: true, supportAutoMap: false,
728
+ noFieldsError: 'Choose a Semantic Query to load its LifeSpace-published parameters.',
729
+ },
730
+ },
731
+ displayOptions: { show: { resource: ['modelRecord'], operation: ['list'] } },
732
+ description: 'Parameters are projected directly from query.capabilityQueries. Calendar local dates/timezone are sent to Core unchanged; Core owns overlap and DST semantics.',
733
+ },
734
+ {
735
+ displayName: 'Semantic Sort Name or ID', name: 'semanticSort', type: 'options',
736
+ typeOptions: { loadOptionsMethod: 'getSemanticSorts', loadOptionsDependsOn: ['spaceId', 'recordType', 'semanticQueryKey'] },
737
+ options: [], default: '',
738
+ displayOptions: { show: { resource: ['modelRecord'], operation: ['list'] } },
739
+ description: 'Optional capability-specific sort published with the selected Semantic Query. Do not combine with field Sorts. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
740
+ },
547
741
  {
548
742
  displayName: 'Return All', name: 'returnAll', type: 'boolean', default: false,
549
743
  displayOptions: { show: { resource: ['modelRecord'], operation: ['list'] } },
@@ -643,7 +837,7 @@ class LifeSpace {
643
837
  var _a;
644
838
  return ({
645
839
  name: `${model.display.plural} (${model.key})`,
646
- value: (0, lifespaceDiscovery_1.encodeRecordTypeSelector)(model.key, model.route),
840
+ value: model.key,
647
841
  description: (_a = model.description) !== null && _a !== void 0 ? _a : undefined,
648
842
  });
649
843
  });
@@ -698,6 +892,64 @@ class LifeSpace {
698
892
  async getBooleanFilterableFields() { return filterFieldOptions(this, ['boolean']); },
699
893
  async getNumericFilterableFields() { return filterFieldOptions(this, ['integer', 'number']); },
700
894
  async getTemporalFilterableFields() { return filterFieldOptions(this, ['date', 'datetime'], true); },
895
+ async getNumericComparisonFields() { return comparisonFieldOptions(this, ['integer', 'number']); },
896
+ async getTemporalComparisonFields() { return comparisonFieldOptions(this, ['date', 'datetime']); },
897
+ async getComparisonOperatorsForCurrentField() {
898
+ var _a, _b;
899
+ const selected = await optionModel(this);
900
+ const { field } = comparisonFieldParts(this.getCurrentNodeParameter('&field'));
901
+ const comparison = (_a = selected === null || selected === void 0 ? void 0 : selected.model.query.comparisons) === null || _a === void 0 ? void 0 : _a.find((entry) => entry.field === field);
902
+ return ((_b = comparison === null || comparison === void 0 ? void 0 : comparison.operators) !== null && _b !== void 0 ? _b : [])
903
+ .filter((operator) => operator.transport === 'explicit')
904
+ .map((operator) => {
905
+ var _a;
906
+ return ({
907
+ name: (_a = COMPARISON_OPERATOR_LABELS[operator.operator]) !== null && _a !== void 0 ? _a : (0, lifespaceDiscovery_1.humanizeKey)(operator.operator),
908
+ value: operator.parameter,
909
+ description: `${operator.operator} · ${operator.transport} · ${operator.parameter}`,
910
+ });
911
+ });
912
+ },
913
+ async getLocalDateWindowFields() {
914
+ var _a;
915
+ const selected = await optionModel(this);
916
+ if (!selected)
917
+ return [];
918
+ return ((_a = selected.model.query.comparisons) !== null && _a !== void 0 ? _a : [])
919
+ .filter((comparison) => comparison.valueType === 'datetime' && comparison.localDateWindow)
920
+ .map((comparison) => {
921
+ var _a;
922
+ const field = selected.model.fields.find((entry) => entry.key === comparison.field);
923
+ return {
924
+ name: ((_a = field === null || field === void 0 ? void 0 : field.title) === null || _a === void 0 ? void 0 : _a.trim()) || (0, lifespaceDiscovery_1.humanizeKey)(comparison.field),
925
+ value: encodeLocalDateWindowSelector(comparison),
926
+ description: `${comparison.source} · [start,end) local calendar window · Core resolves timezone/DST`,
927
+ };
928
+ });
929
+ },
930
+ async getCapabilityQueries() {
931
+ var _a;
932
+ const selected = await optionModel(this);
933
+ return ((_a = selected === null || selected === void 0 ? void 0 : selected.model.query.capabilityQueries) !== null && _a !== void 0 ? _a : []).map((query) => ({
934
+ name: `${(0, lifespaceDiscovery_1.humanizeKey)(query.capability)} · ${(0, lifespaceDiscovery_1.humanizeKey)(query.key)}`,
935
+ value: query.key,
936
+ description: `${query.semantics}${query.recurrenceExpansion === false ? ' · recurrence not expanded' : ''}`,
937
+ }));
938
+ },
939
+ async getSemanticSorts() {
940
+ var _a, _b, _c;
941
+ const selected = await optionModel(this);
942
+ const queryKey = (0, lifespaceDiscovery_1.loadOptionParameter)(this, 'semanticQueryKey');
943
+ const query = (_a = selected === null || selected === void 0 ? void 0 : selected.model.query.capabilityQueries) === null || _a === void 0 ? void 0 : _a.find((entry) => entry.key === queryKey);
944
+ return ((_c = (_b = query === null || query === void 0 ? void 0 : query.ordering) === null || _b === void 0 ? void 0 : _b.values) !== null && _c !== void 0 ? _c : []).map((value) => {
945
+ var _a, _b;
946
+ return ({
947
+ name: value === ((_a = query === null || query === void 0 ? void 0 : query.ordering) === null || _a === void 0 ? void 0 : _a.default) ? `${value} (Default)` : value,
948
+ value,
949
+ description: `${(_b = query === null || query === void 0 ? void 0 : query.key) !== null && _b !== void 0 ? _b : 'semantic query'} ordering`,
950
+ });
951
+ });
952
+ },
701
953
  async getPersonFilterableFields() { return relationFieldOptions(this, undefined, true); },
702
954
  async getEnumValuesForCurrentFilter() {
703
955
  var _a, _b;
@@ -742,10 +994,18 @@ class LifeSpace {
742
994
  ? { fields: action.input.fields.map((field) => mapperField(field, field.required === true)) }
743
995
  : { fields: [] };
744
996
  },
997
+ async getSemanticQueryInputFields() {
998
+ var _a;
999
+ const selected = await optionModel(this);
1000
+ const queryKey = (0, lifespaceDiscovery_1.loadOptionParameter)(this, 'semanticQueryKey');
1001
+ const query = (_a = selected === null || selected === void 0 ? void 0 : selected.model.query.capabilityQueries) === null || _a === void 0 ? void 0 : _a.find((entry) => entry.key === queryKey);
1002
+ return query ? { fields: query.parameters.map(semanticQueryMapperField) } : { fields: [] };
1003
+ },
745
1004
  },
746
1005
  };
747
1006
  }
748
1007
  async execute() {
1008
+ var _a, _b;
749
1009
  const items = this.getInputData();
750
1010
  const output = [];
751
1011
  const credentials = await this.getCredentials('lifeSpaceApi');
@@ -757,15 +1017,15 @@ class LifeSpace {
757
1017
  if (resource === 'modelRecord') {
758
1018
  const operation = this.getNodeParameter('operation', itemIndex);
759
1019
  const rawSpaceId = String(this.getNodeParameter('spaceId', itemIndex));
760
- const recordType = (0, lifespaceDiscovery_1.decodeRecordTypeSelector)(this.getNodeParameter('recordType', itemIndex));
761
- if (!recordType) {
1020
+ const recordType = (0, lifespaceDiscovery_1.decodeRecordTypeSelector)(this.getNodeParameter('recordType', itemIndex, ''));
1021
+ const legacyModelRoute = String((_a = this.getNodeParameter('modelRoute', itemIndex, '')) !== null && _a !== void 0 ? _a : '').trim();
1022
+ const rawModelKey = (_b = recordType === null || recordType === void 0 ? void 0 : recordType.modelKey) !== null && _b !== void 0 ? _b : LEGACY_MODEL_ROUTE_TO_KEY[legacyModelRoute];
1023
+ if (!rawModelKey) {
762
1024
  throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'LifeSpace Record Type selector is invalid. Choose a Record Type from Discovery or pass a Trigger recordType value.', { itemIndex });
763
1025
  }
764
- const rawModelKey = recordType.modelKey;
765
- const rawModelRoute = recordType.route;
766
1026
  const spaceId = encodeURIComponent(rawSpaceId);
767
- const modelRoute = encodeURIComponent(rawModelRoute);
768
- const collectionPath = `/spaces/${spaceId}/${modelRoute}`;
1027
+ const modelKey = encodeURIComponent(rawModelKey);
1028
+ const collectionPath = `/spaces/${spaceId}/models/${modelKey}/records`;
769
1029
  if (operation === 'list') {
770
1030
  const returnAll = this.getNodeParameter('returnAll', itemIndex, false);
771
1031
  if (!returnAll) {