n8n-nodes-lifespace 0.1.6 → 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
@@ -74,9 +74,19 @@ Record Type is the LifeSpace `modelKey` (for example `task`). Design-time option
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.32.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
 
@@ -95,6 +105,9 @@ The UX depends on these Kernel capabilities:
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.
97
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.
98
111
 
99
112
  The adapter prefers the `0.27+` progressive flow while configuring a node:
100
113
 
@@ -199,9 +212,14 @@ The normal UI supports:
199
212
 
200
213
  - optional **Search**;
201
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;
202
218
  - **Return All** to follow `nextCursor` automatically;
203
219
  - **Limit** when Return All is disabled.
204
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
+
205
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.
206
224
 
207
225
  Advanced **Options** contain manual **Cursor** as an escape hatch.
@@ -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[][]>;
@@ -9,6 +9,75 @@ const LEGACY_MODEL_ROUTE_TO_KEY = Object.freeze({
9
9
  'day-records': 'day_record',
10
10
  events: 'event',
11
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
+ }
12
81
  function parseJsonObject(context, itemIndex, value, fieldName) {
13
82
  const parsed = typeof value === 'string' ? JSON.parse(value) : value;
14
83
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
@@ -102,14 +171,15 @@ function normalizeActionInput(context, itemIndex, value, fields) {
102
171
  return result;
103
172
  }
104
173
  function queryParameters(context, itemIndex, limit, cursorOverride) {
105
- 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;
106
175
  const qs = {};
107
176
  const search = String(context.getNodeParameter('search', itemIndex, '')).trim();
108
177
  const options = context.getNodeParameter('options', itemIndex, {});
109
178
  const configuredSorts = context.getNodeParameter('sorts.sort', itemIndex, []);
110
- const legacySortField = String((_a = options.sortField) !== null && _a !== void 0 ? _a : '').trim();
111
- const legacySortDirection = String((_b = options.sortDirection) !== null && _b !== void 0 ? _b : 'desc').trim();
112
- 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();
113
183
  const cursor = cursorOverride !== null && cursorOverride !== void 0 ? cursorOverride : configuredCursor;
114
184
  const filters = context.getNodeParameter('filters.filter', itemIndex, []);
115
185
  if (search)
@@ -117,10 +187,10 @@ function queryParameters(context, itemIndex, limit, cursorOverride) {
117
187
  const orderedSorts = [];
118
188
  const usedSortFields = new Set();
119
189
  for (const sort of configuredSorts) {
120
- 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();
121
191
  if (!field)
122
192
  continue;
123
- 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();
124
194
  if (direction !== 'asc' && direction !== 'desc') {
125
195
  throw new n8n_workflow_1.NodeOperationError(context.getNode(), `Sort direction for ${field} must be asc or desc`, { itemIndex });
126
196
  }
@@ -130,56 +200,64 @@ function queryParameters(context, itemIndex, limit, cursorOverride) {
130
200
  usedSortFields.add(field);
131
201
  orderedSorts.push(`${field}:${direction}`);
132
202
  }
133
- 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)
134
209
  qs.sort = orderedSorts[0];
135
- if (orderedSorts.length > 1)
210
+ if (!semanticSort && orderedSorts.length > 1)
136
211
  qs.sort = orderedSorts;
137
- if (orderedSorts.length === 0 && legacySortField)
212
+ if (!semanticSort && orderedSorts.length === 0 && legacySortField)
138
213
  qs.sort = `${legacySortField}:${legacySortDirection}`;
139
214
  qs.limit = limit;
140
215
  if (cursor)
141
216
  qs.cursor = cursor;
142
217
  const usedKeys = new Set();
143
- const setFilter = (field, operator, value) => {
144
- const key = operator === 'from' ? `${field}From` : operator === 'to' ? `${field}To` : field;
218
+ const setQueryParameter = (key, value) => {
145
219
  if (usedKeys.has(key)) {
146
- 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 });
147
221
  }
148
222
  usedKeys.add(key);
149
223
  qs[key] = typeof value === 'boolean' ? String(value) : value;
150
224
  };
225
+ const setFilter = (field, operator, value) => {
226
+ const key = operator === 'from' ? `${field}From` : operator === 'to' ? `${field}To` : field;
227
+ setQueryParameter(key, value);
228
+ };
151
229
  for (const filter of filters) {
152
- const field = String((_f = filter.field) !== null && _f !== void 0 ? _f : '').trim();
153
- const value = String((_g = filter.value) !== null && _g !== void 0 ? _g : '');
154
- 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';
155
233
  if (field && value !== '')
156
234
  setFilter(field, operator, value);
157
235
  }
158
236
  const typedFilters = context.getNodeParameter('filters', itemIndex, {});
159
- for (const row of ((_j = typedFilters.text) !== null && _j !== void 0 ? _j : [])) {
160
- 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();
161
239
  if (field && row.value !== undefined && row.value !== '')
162
240
  setFilter(field, 'exact', String(row.value));
163
241
  }
164
- for (const row of ((_l = typedFilters.enum) !== null && _l !== void 0 ? _l : [])) {
165
- 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();
166
244
  const values = Array.isArray(row.values) ? row.values.map(String).filter(Boolean) : [];
167
245
  if (field && values.length)
168
246
  setFilter(field, 'exact', values.join(','));
169
247
  }
170
- for (const row of ((_o = typedFilters.boolean) !== null && _o !== void 0 ? _o : [])) {
171
- 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();
172
250
  if (field)
173
251
  setFilter(field, 'exact', Boolean(row.value));
174
252
  }
175
- for (const row of ((_q = typedFilters.number) !== null && _q !== void 0 ? _q : [])) {
176
- 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();
177
255
  const value = Number(row.value);
178
256
  if (field && Number.isFinite(value))
179
- 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);
180
258
  }
181
- for (const row of ((_t = typedFilters.temporal) !== null && _t !== void 0 ? _t : [])) {
182
- 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();
183
261
  if (!encoded || row.value === undefined || row.value === '')
184
262
  continue;
185
263
  const separator = encoded.indexOf(':');
@@ -188,14 +266,59 @@ function queryParameters(context, itemIndex, limit, cursorOverride) {
188
266
  const raw = String(row.value);
189
267
  const value = fieldType === 'date' ? dateOnlyValue(context, itemIndex, raw, field) : raw;
190
268
  if (value !== null)
191
- 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);
192
270
  }
193
- for (const row of ((_w = typedFilters.person) !== null && _w !== void 0 ? _w : [])) {
194
- const field = String((_x = row.field) !== null && _x !== void 0 ? _x : '').trim();
195
- 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();
196
274
  if (field && target)
197
275
  setFilter(field, 'exact', target);
198
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
+ }
199
322
  return qs;
200
323
  }
201
324
  function queryPage(context, itemIndex, response) {
@@ -349,6 +472,24 @@ async function filterFieldOptions(context, types, encodeType = false) {
349
472
  });
350
473
  });
351
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
+ }
352
493
  async function relationFieldOptions(context, cardinality, filterableOnly = false) {
353
494
  const selected = await optionModel(context);
354
495
  if (!selected)
@@ -537,6 +678,16 @@ class LifeSpace {
537
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' },
538
679
  { displayName: 'Value', name: 'value', type: 'dateTime', default: '', required: true },
539
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
+ ] },
540
691
  { displayName: 'Person Filter', name: 'person', values: [
541
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 },
542
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 },
@@ -548,6 +699,45 @@ class LifeSpace {
548
699
  ] },
549
700
  ],
550
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
+ },
551
741
  {
552
742
  displayName: 'Return All', name: 'returnAll', type: 'boolean', default: false,
553
743
  displayOptions: { show: { resource: ['modelRecord'], operation: ['list'] } },
@@ -702,6 +892,64 @@ class LifeSpace {
702
892
  async getBooleanFilterableFields() { return filterFieldOptions(this, ['boolean']); },
703
893
  async getNumericFilterableFields() { return filterFieldOptions(this, ['integer', 'number']); },
704
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
+ },
705
953
  async getPersonFilterableFields() { return relationFieldOptions(this, undefined, true); },
706
954
  async getEnumValuesForCurrentFilter() {
707
955
  var _a, _b;
@@ -746,6 +994,13 @@ class LifeSpace {
746
994
  ? { fields: action.input.fields.map((field) => mapperField(field, field.required === true)) }
747
995
  : { fields: [] };
748
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
+ },
749
1004
  },
750
1005
  };
751
1006
  }