n8n-nodes-lifespace 0.1.3 → 0.1.5

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,7 +70,9 @@ 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
- Calendar-backed models use canonical `capabilityBindings.calendar` field roles instead of Event-specific field names. Date-only values are normalized to `YYYY-MM-DD`, and contradictory all-day/timed state is rejected locally before the mutation request while Core remains the final validation authority.
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.
74
+
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.
74
76
 
75
77
  ## LifeSpace contract compatibility
76
78
 
@@ -93,17 +95,21 @@ The UX depends on these Kernel capabilities:
93
95
  - `0.30.0`: explicit paginated Change History collection and Model Control Plane ownership split;
94
96
  - `0.31.0`: Integration/Eventing wire representation moves to the independent Integration/Eventing `0.1.0` contract while Core remains the Runtime authority.
95
97
 
96
- The adapter prefers the `0.27+` progressive flow:
98
+ The adapter prefers the `0.27+` progressive flow while configuring a node:
97
99
 
98
100
  ```text
99
101
  GET /me/_discovery/inventory
100
102
  -> choose current Space / Record Type
101
103
  -> GET /spaces/{spaceId}/_discovery/models/{modelKey}
102
104
  -> relation target lookup / reference resolution only when needed
103
- -> canonical Runtime request
105
+ -> persist stable workflow configuration
104
106
  ```
105
107
 
106
- The legacy aggregate `GET /me/_discovery` remains an intentional compatibility fallback. Neither cached Discovery nor relation lookup is treated as authorization proof; every mutation still goes through canonical LifeSpace Runtime enforcement.
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.
109
+
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.
111
+
112
+ 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.
107
113
 
108
114
  Ordinary Record CRUD/Action routes remain model-contract surfaces derived from published Model Definitions; the n8n adapter does not maintain a second copy of those schemas.
109
115
 
@@ -116,10 +122,11 @@ Examples:
116
122
  ```text
117
123
  {{$json.spaceId}}
118
124
  {{$json.recordId}}
125
+ {{$json.recordType}}
119
126
  {{$vars.lifeSpaceRecordType}}
120
127
  ```
121
128
 
122
- Discovery-backed selectors such as **Space**, **Record Type**, **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.
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.
123
130
 
124
131
  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.
125
132
 
@@ -173,13 +180,15 @@ Older compatible Discovery responses without relation lookup metadata retain the
173
180
 
174
181
  The current n8n UI loads bounded relation options when the relevant field is configured. Very large target sets should use expressions with stable IDs until n8n exposes a searchable dynamic relation option surface that can consume LifeSpace's paginated/searchable lookup directly.
175
182
 
183
+ At execution time Create uses the already-configured Record Type selector and sends the canonical mutation directly. This avoids paying a current-principal inventory round trip merely to rediscover whether adapter-side semantic preflight would improve an error message. Core remains the final authority for schema, Capability, Policy and authorization checks.
184
+
176
185
  ### Update and Delete
177
186
 
178
187
  LifeSpace uses optimistic concurrency.
179
188
 
180
189
  By default the node reads the current Record version immediately before Update/Delete and sends that version with the mutation. This keeps the ordinary n8n UI free from mandatory internal `version` entry while preserving stale-write protection for the actual mutation race.
181
190
 
182
- For Calendar-backed Update, the node also combines the current record with the proposed patch and checks the discovered all-day/timed field roles before sending the mutation. Core validation remains authoritative.
191
+ Update does not add a Runtime Discovery request before the mutation. When no explicit version is configured, the current-record read is for optimistic concurrency rather than semantic discovery. Core validates Calendar and other Capability semantics against the resulting mutation.
183
192
 
184
193
  If a workflow intentionally needs to bind a known version, add **Concurrency Options → Version**.
185
194
 
@@ -204,7 +213,7 @@ Choose an Action from Runtime Discovery.
204
213
 
205
214
  **Action Input** contains only semantic/domain inputs. LifeSpace concurrency metadata is not rendered as a business field. For the current `record-version` contract, the node reads the current Record version immediately before Action execution and sends it using the transport declared by Runtime Discovery.
206
215
 
207
- Execution-time Action metadata uses the same progressive inventory + selected-model detail path rather than loading the full cross-Space Discovery document.
216
+ Execution-time Action metadata is loaded directly from the selected model's static semantic-detail endpoint rather than first loading the broad current-principal inventory. The static detail is reused for repeated items of the same Space/Record Type during one node execution. It is semantic input only, not cached authority; Core rechecks current Action authority on every invocation.
208
217
 
209
218
  This means actions such as `complete` / `reopen` no longer ask users to type an internal version value.
210
219
 
@@ -247,6 +256,8 @@ One Webhook Endpoint can therefore carry events for multiple Record Types throug
247
256
 
248
257
  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.
249
258
 
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.
260
+
250
261
  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.
251
262
 
252
263
  ## What the package provides
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LifeSpace = void 0;
4
4
  const n8n_workflow_1 = require("n8n-workflow");
5
- const calendarMutation_1 = require("../calendarMutation");
6
5
  const lifespaceDiscovery_1 = require("../lifespaceDiscovery");
7
6
  function parseJsonObject(context, itemIndex, value, fieldName) {
8
7
  const parsed = typeof value === 'string' ? JSON.parse(value) : value;
@@ -272,26 +271,24 @@ async function currentRecord(context, itemIndex, baseUrl, recordPath) {
272
271
  async function currentRecordVersion(context, itemIndex, baseUrl, recordPath) {
273
272
  return recordVersion(context, itemIndex, await currentRecord(context, itemIndex, baseUrl, recordPath));
274
273
  }
275
- async function mutationVersion(context, itemIndex, baseUrl, recordPath, loadedRecord) {
274
+ async function mutationVersion(context, itemIndex, baseUrl, recordPath) {
276
275
  const options = context.getNodeParameter('mutationOptions', itemIndex, {});
277
276
  const configuredVersion = options.version;
278
277
  if (typeof configuredVersion === 'number' && Number.isInteger(configuredVersion) && configuredVersion >= 1) {
279
278
  return configuredVersion;
280
279
  }
281
- return loadedRecord
282
- ? recordVersion(context, itemIndex, loadedRecord)
283
- : currentRecordVersion(context, itemIndex, baseUrl, recordPath);
280
+ return currentRecordVersion(context, itemIndex, baseUrl, recordPath);
284
281
  }
285
- async function executionModel(context, itemIndex, baseUrl, spaceId, modelRoute) {
286
- const discovery = await (0, lifespaceDiscovery_1.loadExecutionRuntimeDiscovery)(context, baseUrl, spaceId, modelRoute);
287
- const model = (0, lifespaceDiscovery_1.discoveryModel)(discovery, spaceId, modelRoute);
282
+ async function executionModel(context, itemIndex, baseUrl, spaceId, modelKey) {
283
+ const discovery = await (0, lifespaceDiscovery_1.loadExecutionRuntimeDiscovery)(context, baseUrl, spaceId, modelKey);
284
+ const model = (0, lifespaceDiscovery_1.discoveryModel)(discovery, spaceId, modelKey);
288
285
  if (!model) {
289
- throw new n8n_workflow_1.NodeOperationError(context.getNode(), `LifeSpace Record Type ${modelRoute} is not available`, { itemIndex });
286
+ throw new n8n_workflow_1.NodeOperationError(context.getNode(), `LifeSpace Record Type ${modelKey} is not available`, { itemIndex });
290
287
  }
291
288
  return model;
292
289
  }
293
- async function actionBodyWithConcurrency(context, itemIndex, baseUrl, spaceId, modelRoute, recordPath, actionKey, semanticInput) {
294
- const model = await executionModel(context, itemIndex, baseUrl, spaceId, modelRoute);
290
+ async function actionBodyWithConcurrency(context, itemIndex, baseUrl, spaceId, modelKey, recordPath, actionKey, semanticInput) {
291
+ const model = await executionModel(context, itemIndex, baseUrl, spaceId, modelKey);
295
292
  const action = model.actions.find((entry) => entry.key === actionKey);
296
293
  if (!action) {
297
294
  throw new n8n_workflow_1.NodeOperationError(context.getNode(), `LifeSpace Action ${actionKey} is not available`, { itemIndex });
@@ -316,12 +313,20 @@ function actionOption(action) {
316
313
  };
317
314
  }
318
315
  async function optionModel(context) {
319
- const spaceId = String(context.getNodeParameter('spaceId', '')).trim();
320
- const modelRoute = String(context.getNodeParameter('modelRoute', '')).trim();
321
- if (!spaceId || !modelRoute)
316
+ const spaceId = (0, lifespaceDiscovery_1.loadOptionParameter)(context, 'spaceId');
317
+ const recordTypeValue = (0, lifespaceDiscovery_1.loadOptionParameter)(context, 'recordType');
318
+ const legacyModelRoute = recordTypeValue ? '' : (0, lifespaceDiscovery_1.loadOptionParameter)(context, 'modelRoute');
319
+ if (!spaceId || (!recordTypeValue && !legacyModelRoute))
322
320
  return null;
321
+ const recordType = recordTypeValue ? (0, lifespaceDiscovery_1.decodeRecordTypeSelector)(recordTypeValue) : null;
322
+ if (recordTypeValue && !recordType) {
323
+ 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
+ }
323
325
  const discovery = await lifespaceDiscovery_1.loadRuntimeDiscovery.call(context);
324
- const model = (0, lifespaceDiscovery_1.discoveryModel)(discovery, spaceId, modelRoute);
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);
325
330
  return model ? { model, spaceId } : null;
326
331
  }
327
332
  async function filterFieldOptions(context, types, encodeType = false) {
@@ -344,7 +349,7 @@ async function relationFieldOptions(context, cardinality, filterableOnly = false
344
349
  const selected = await optionModel(context);
345
350
  if (!selected)
346
351
  return [];
347
- const operation = String(context.getNodeParameter('operation', 'create'));
352
+ const operation = (0, lifespaceDiscovery_1.loadOptionParameter)(context, 'operation') || 'create';
348
353
  const filterable = new Set(selected.model.query.filterable);
349
354
  return selected.model.fields
350
355
  .filter((field) => { var _a; return ((_a = field.relation) === null || _a === void 0 ? void 0 : _a.lookup.supported) === true; })
@@ -419,7 +424,7 @@ class LifeSpace {
419
424
  description: 'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
420
425
  },
421
426
  {
422
- displayName: 'Record Type Name or ID', name: 'modelRoute', type: 'options',
427
+ displayName: 'Record Type Name or ID', name: 'recordType', type: 'options',
423
428
  typeOptions: { loadOptionsMethod: 'getRecordTypes', loadOptionsDependsOn: ['spaceId', 'operation'] },
424
429
  options: [], default: '', required: true, displayOptions: { show: { resource: ['modelRecord'] } },
425
430
  description: 'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
@@ -432,7 +437,7 @@ class LifeSpace {
432
437
  displayName: 'Fields', name: 'fields', type: 'resourceMapper',
433
438
  default: { mappingMode: 'defineBelow', value: null }, noDataExpression: true, required: true,
434
439
  typeOptions: {
435
- loadOptionsDependsOn: ['spaceId', 'modelRoute', 'operation'],
440
+ loadOptionsDependsOn: ['spaceId', 'recordType', 'operation'],
436
441
  resourceMapper: {
437
442
  resourceMapperMethod: 'getRecordFields', mode: 'add',
438
443
  fieldWords: { singular: 'field', plural: 'fields' }, addAllFields: true, supportAutoMap: false,
@@ -450,7 +455,7 @@ class LifeSpace {
450
455
  displayName: 'Date', name: 'date', values: [
451
456
  {
452
457
  displayName: 'Field Name or ID', name: 'field', type: 'options',
453
- typeOptions: { loadOptionsMethod: 'getWritableDateFields', loadOptionsDependsOn: ['spaceId', 'modelRoute', 'operation'] },
458
+ typeOptions: { loadOptionsMethod: 'getWritableDateFields', loadOptionsDependsOn: ['spaceId', 'recordType', 'operation'] },
454
459
  options: [], default: '', required: true,
455
460
  description: 'Choose a LifeSpace date field. The node submits calendar-date YYYY-MM-DD values. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
456
461
  },
@@ -466,12 +471,12 @@ class LifeSpace {
466
471
  {
467
472
  displayName: 'Field Name or ID', name: 'field', type: 'options',
468
473
  description: 'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
469
- typeOptions: { loadOptionsMethod: 'getSingleRelationFields', loadOptionsDependsOn: ['spaceId', 'modelRoute', 'operation'] },
474
+ typeOptions: { loadOptionsMethod: 'getSingleRelationFields', loadOptionsDependsOn: ['spaceId', 'recordType', 'operation'] },
470
475
  options: [], default: '', required: true,
471
476
  },
472
477
  {
473
478
  displayName: 'Target Name or ID', name: 'target', type: 'options',
474
- typeOptions: { loadOptionsMethod: 'getRelationTargetsForCurrentField', loadOptionsDependsOn: ['spaceId', 'modelRoute', '&field'] },
479
+ typeOptions: { loadOptionsMethod: 'getRelationTargetsForCurrentField', loadOptionsDependsOn: ['spaceId', 'recordType', '&field'] },
475
480
  options: [], default: '',
476
481
  description: 'Choose an authorized relation target, or use an expression with a stable LifeSpace ID. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
477
482
  },
@@ -485,12 +490,12 @@ class LifeSpace {
485
490
  {
486
491
  displayName: 'Field Name or ID', name: 'field', type: 'options',
487
492
  description: 'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
488
- typeOptions: { loadOptionsMethod: 'getMultiRelationFields', loadOptionsDependsOn: ['spaceId', 'modelRoute', 'operation'] },
493
+ typeOptions: { loadOptionsMethod: 'getMultiRelationFields', loadOptionsDependsOn: ['spaceId', 'recordType', 'operation'] },
489
494
  options: [], default: '', required: true,
490
495
  },
491
496
  {
492
497
  displayName: 'Targets Names or IDs', name: 'targets', type: 'multiOptions',
493
- typeOptions: { loadOptionsMethod: 'getRelationTargetsForCurrentField', loadOptionsDependsOn: ['spaceId', 'modelRoute', '&field'] },
498
+ typeOptions: { loadOptionsMethod: 'getRelationTargetsForCurrentField', loadOptionsDependsOn: ['spaceId', 'recordType', '&field'] },
494
499
  options: [], default: [],
495
500
  description: 'Choose authorized relation targets, or use an expression with stable LifeSpace IDs. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
496
501
  },
@@ -507,30 +512,30 @@ class LifeSpace {
507
512
  displayOptions: { show: { resource: ['modelRecord'], operation: ['list'] } },
508
513
  options: [
509
514
  { displayName: 'Text Filter', name: 'text', values: [
510
- { 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: 'getTextFilterableFields', loadOptionsDependsOn: ['spaceId', 'modelRoute'] }, options: [], default: '', required: true },
515
+ { 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: 'getTextFilterableFields', loadOptionsDependsOn: ['spaceId', 'recordType'] }, options: [], default: '', required: true },
511
516
  { displayName: 'Value', name: 'value', type: 'string', default: '', required: true },
512
517
  ] },
513
518
  { displayName: 'Enum Filter', name: 'enum', values: [
514
- { 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: 'getEnumFilterableFields', loadOptionsDependsOn: ['spaceId', 'modelRoute'] }, options: [], default: '', required: true },
515
- { displayName: 'Value Names or IDs', name: 'values', type: 'multiOptions', description: 'Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>', typeOptions: { loadOptionsMethod: 'getEnumValuesForCurrentFilter', loadOptionsDependsOn: ['spaceId', 'modelRoute', '&field'] }, options: [], default: [], required: true },
519
+ { 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: 'getEnumFilterableFields', loadOptionsDependsOn: ['spaceId', 'recordType'] }, options: [], default: '', required: true },
520
+ { displayName: 'Value Names or IDs', name: 'values', type: 'multiOptions', description: 'Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>', typeOptions: { loadOptionsMethod: 'getEnumValuesForCurrentFilter', loadOptionsDependsOn: ['spaceId', 'recordType', '&field'] }, options: [], default: [], required: true },
516
521
  ] },
517
522
  { displayName: 'Boolean Filter', name: 'boolean', values: [
518
- { 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: 'getBooleanFilterableFields', loadOptionsDependsOn: ['spaceId', 'modelRoute'] }, options: [], default: '', required: true },
523
+ { 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: 'getBooleanFilterableFields', loadOptionsDependsOn: ['spaceId', 'recordType'] }, options: [], default: '', required: true },
519
524
  { displayName: 'Value', name: 'value', type: 'boolean', default: true },
520
525
  ] },
521
526
  { displayName: 'Number Filter', name: 'number', values: [
522
- { 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: 'getNumericFilterableFields', loadOptionsDependsOn: ['spaceId', 'modelRoute'] }, options: [], default: '', required: true },
527
+ { 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: 'getNumericFilterableFields', loadOptionsDependsOn: ['spaceId', 'recordType'] }, options: [], default: '', required: true },
523
528
  { 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' },
524
529
  { displayName: 'Value', name: 'value', type: 'number', default: 0, required: true },
525
530
  ] },
526
531
  { displayName: 'Date / Time Filter', name: 'temporal', values: [
527
- { 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: 'getTemporalFilterableFields', loadOptionsDependsOn: ['spaceId', 'modelRoute'] }, options: [], default: '', required: true },
532
+ { 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: 'getTemporalFilterableFields', loadOptionsDependsOn: ['spaceId', 'recordType'] }, options: [], default: '', required: true },
528
533
  { 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' },
529
534
  { displayName: 'Value', name: 'value', type: 'dateTime', default: '', required: true },
530
535
  ] },
531
536
  { displayName: 'Person Filter', name: 'person', values: [
532
- { 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', 'modelRoute'] }, options: [], default: '', required: true },
533
- { 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', 'modelRoute', '&field'] }, options: [], default: '', required: true },
537
+ { 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
+ { 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 },
534
539
  ] },
535
540
  { displayName: 'Raw / Legacy Filter', name: 'filter', values: [
536
541
  { displayName: 'Field Name or ID', name: 'field', type: 'options', typeOptions: { loadOptionsMethod: 'getFilterableFields' }, options: [], default: '', required: true, description: 'Compatibility and unsupported relation escape hatch. Prefer the typed filter variants above. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.' },
@@ -572,7 +577,7 @@ class LifeSpace {
572
577
  },
573
578
  {
574
579
  displayName: 'Action Name or ID', name: 'actionKey', type: 'options',
575
- typeOptions: { loadOptionsMethod: 'getActions', loadOptionsDependsOn: ['spaceId', 'modelRoute'] },
580
+ typeOptions: { loadOptionsMethod: 'getActions', loadOptionsDependsOn: ['spaceId', 'recordType'] },
576
581
  options: [], default: '', required: true,
577
582
  displayOptions: { show: { resource: ['modelRecord'], operation: ['executeAction'] } },
578
583
  description: 'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
@@ -581,7 +586,7 @@ class LifeSpace {
581
586
  displayName: 'Action Input', name: 'actionInput', type: 'resourceMapper',
582
587
  default: { mappingMode: 'defineBelow', value: null }, noDataExpression: true,
583
588
  typeOptions: {
584
- loadOptionsDependsOn: ['spaceId', 'modelRoute', 'actionKey'],
589
+ loadOptionsDependsOn: ['spaceId', 'recordType', 'actionKey'],
585
590
  resourceMapper: { resourceMapperMethod: 'getActionInputFields', mode: 'add', fieldWords: { singular: 'input', plural: 'inputs' }, addAllFields: true, supportAutoMap: false, noFieldsError: 'This LifeSpace Action has no semantic input fields.' },
586
591
  },
587
592
  displayOptions: { show: { resource: ['modelRecord'], operation: ['executeAction'] } },
@@ -622,19 +627,26 @@ class LifeSpace {
622
627
  });
623
628
  },
624
629
  async getRecordTypes() {
625
- const spaceId = String(this.getNodeParameter('spaceId', '')).trim();
630
+ const spaceId = (0, lifespaceDiscovery_1.loadOptionParameter)(this, 'spaceId');
626
631
  if (!spaceId)
627
632
  return [];
628
633
  const discovery = await lifespaceDiscovery_1.loadRuntimeDiscovery.call(this);
629
634
  const space = (0, lifespaceDiscovery_1.discoverySpace)(discovery, spaceId);
630
635
  if (!space)
631
636
  return [];
632
- const operation = String(this.getNodeParameter('operation', 'list'));
637
+ const operation = (0, lifespaceDiscovery_1.loadOptionParameter)(this, 'operation') || 'list';
633
638
  return space.models
634
639
  .filter((model) => operation === 'executeAction'
635
640
  ? model.actions.length > 0
636
641
  : model.access.includes(requiredAccessForOperation(operation)))
637
- .map((model) => { var _a; return ({ name: `${model.display.plural} (${model.route})`, value: model.route, description: (_a = model.description) !== null && _a !== void 0 ? _a : undefined }); });
642
+ .map((model) => {
643
+ var _a;
644
+ return ({
645
+ name: `${model.display.plural} (${model.key})`,
646
+ value: (0, lifespaceDiscovery_1.encodeRecordTypeSelector)(model.key, model.route),
647
+ description: (_a = model.description) !== null && _a !== void 0 ? _a : undefined,
648
+ });
649
+ });
638
650
  },
639
651
  async getActions() {
640
652
  const selected = await optionModel(this);
@@ -658,7 +670,7 @@ class LifeSpace {
658
670
  const selected = await optionModel(this);
659
671
  if (!selected)
660
672
  return [];
661
- const operation = String(this.getNodeParameter('operation', 'create'));
673
+ const operation = (0, lifespaceDiscovery_1.loadOptionParameter)(this, 'operation') || 'create';
662
674
  return selected.model.fields
663
675
  .filter((field) => field.type === 'date' && !field.readOnly && (operation !== 'update' || !field.immutable))
664
676
  .map((field) => { var _a; return ({ name: ((_a = field.title) === null || _a === void 0 ? void 0 : _a.trim()) || (0, lifespaceDiscovery_1.humanizeKey)(field.key), value: field.key, description: field.description }); });
@@ -713,7 +725,7 @@ class LifeSpace {
713
725
  const selected = await optionModel(this);
714
726
  if (!selected)
715
727
  return { fields: [] };
716
- const operation = String(this.getNodeParameter('operation', 'create'));
728
+ const operation = (0, lifespaceDiscovery_1.loadOptionParameter)(this, 'operation') || 'create';
717
729
  const writableFields = selected.model.fields
718
730
  .filter((field) => !field.readOnly && (operation !== 'update' || !field.immutable))
719
731
  .filter((field) => field.type !== 'date')
@@ -724,7 +736,7 @@ class LifeSpace {
724
736
  },
725
737
  async getActionInputFields() {
726
738
  const selected = await optionModel(this);
727
- const actionKey = String(this.getNodeParameter('actionKey', '')).trim();
739
+ const actionKey = (0, lifespaceDiscovery_1.loadOptionParameter)(this, 'actionKey');
728
740
  const action = selected === null || selected === void 0 ? void 0 : selected.model.actions.find((entry) => entry.key === actionKey);
729
741
  return action
730
742
  ? { fields: action.input.fields.map((field) => mapperField(field, field.required === true)) }
@@ -734,7 +746,6 @@ class LifeSpace {
734
746
  };
735
747
  }
736
748
  async execute() {
737
- var _a;
738
749
  const items = this.getInputData();
739
750
  const output = [];
740
751
  const credentials = await this.getCredentials('lifeSpaceApi');
@@ -746,7 +757,12 @@ class LifeSpace {
746
757
  if (resource === 'modelRecord') {
747
758
  const operation = this.getNodeParameter('operation', itemIndex);
748
759
  const rawSpaceId = String(this.getNodeParameter('spaceId', itemIndex));
749
- const rawModelRoute = String(this.getNodeParameter('modelRoute', itemIndex));
760
+ const recordType = (0, lifespaceDiscovery_1.decodeRecordTypeSelector)(this.getNodeParameter('recordType', itemIndex));
761
+ if (!recordType) {
762
+ 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
+ }
764
+ const rawModelKey = recordType.modelKey;
765
+ const rawModelRoute = recordType.route;
750
766
  const spaceId = encodeURIComponent(rawSpaceId);
751
767
  const modelRoute = encodeURIComponent(rawModelRoute);
752
768
  const collectionPath = `/spaces/${spaceId}/${modelRoute}`;
@@ -787,8 +803,6 @@ class LifeSpace {
787
803
  }
788
804
  else if (operation === 'create') {
789
805
  const body = mutationMappedValues(this, itemIndex);
790
- const model = await executionModel(this, itemIndex, baseUrl, rawSpaceId, rawModelRoute);
791
- (0, calendarMutation_1.validateCalendarMutation)(this, itemIndex, model, body);
792
806
  response = await this.helpers.httpRequestWithAuthentication.call(this, 'lifeSpaceApi', { method: 'POST', url: `${baseUrl}${collectionPath}`, body, json: true });
793
807
  }
794
808
  else {
@@ -800,17 +814,12 @@ class LifeSpace {
800
814
  }
801
815
  else if (operation === 'update') {
802
816
  const body = mutationMappedValues(this, itemIndex);
803
- const model = await executionModel(this, itemIndex, baseUrl, rawSpaceId, rawModelRoute);
804
- const loadedRecord = ((_a = model.capabilityBindings) === null || _a === void 0 ? void 0 : _a.calendar)
805
- ? await currentRecord(this, itemIndex, baseUrl, recordPath)
806
- : undefined;
807
- (0, calendarMutation_1.validateCalendarMutation)(this, itemIndex, model, body, loadedRecord);
808
817
  options = {
809
818
  method: 'PATCH',
810
819
  url: `${baseUrl}${recordPath}`,
811
820
  body: {
812
821
  ...body,
813
- version: await mutationVersion(this, itemIndex, baseUrl, recordPath, loadedRecord),
822
+ version: await mutationVersion(this, itemIndex, baseUrl, recordPath),
814
823
  },
815
824
  json: true,
816
825
  };
@@ -829,7 +838,7 @@ class LifeSpace {
829
838
  options = {
830
839
  method: 'POST',
831
840
  url: `${baseUrl}${recordPath}/actions/${actionKey}`,
832
- body: await actionBodyWithConcurrency(this, itemIndex, baseUrl, rawSpaceId, rawModelRoute, recordPath, rawActionKey, mappedValue(this, itemIndex, 'actionInput')),
841
+ body: await actionBodyWithConcurrency(this, itemIndex, baseUrl, rawSpaceId, rawModelKey, recordPath, rawActionKey, mappedValue(this, itemIndex, 'actionInput')),
833
842
  json: true,
834
843
  };
835
844
  }