@nodegx/mcp 0.2.4 → 0.2.6

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.
@@ -7928,11 +7928,9 @@
7928
7928
  ],
7929
7929
  "dynamicPorts": {
7930
7930
  "mechanisms": [
7931
- "runtime-discovered",
7932
- "editor-adapter"
7931
+ "runtime-discovered"
7933
7932
  ],
7934
- "description": "Filter/sort input ports are generated by the editor from the visual query (QueryRecordsAdapter) and the selected class schema.",
7935
- "editorAdapter": "QueryRecordsAdapter"
7933
+ "description": "Every input is drawn from the parameters, so the static port list is empty: \"collectionName\" (the class), \"visualFilter\", \"visualSort\", \"storageEnableLimit\" + \"storageLimit\"/\"storageSkip\", \"storageEnableCount\", \"search\", \"realtime\", and the \"runOnChange-<input>\" boxes. A server-side filter is \"visualFilter\": {\"combinator\": \"and\", \"rules\": [{\"property\": \"<field>\", \"operator\": \"equal to\", \"input\": \"<name>\"}]}; each rule's \"input\" mints the input port \"qp-<name>\" a connection feeds, or the rule carries a literal \"value\" instead. Any other parameter name is stored and never read."
7936
7934
  },
7937
7935
  "parameterEncoding": {
7938
7936
  "known": true,
@@ -8057,8 +8055,9 @@
8057
8055
  "changedRecords": "All records of the last batch; for deletes, an array of primary-key strings.",
8058
8056
  "changedRecordId": "Primary key of the first affected record, across all three event types."
8059
8057
  },
8060
- "runtimeBehavior": "All *inputs* are runtime-determined (runtime-discovered + editor-adapter): the class choice, filter values, sort keys and paging inputs are generated from the project's database schema and the visual filter you build in the editor — e.g. each 'value' placeholder in a filter becomes its own input port, and actions like fetch ('storageFetch') are registered on demand. A generator cannot enumerate them statically; an authoring tool should set `collectionName` and filter parameters, and may wire signals to fetch-related inputs which the runtime registers when used.",
8058
+ "runtimeBehavior": "Every input is drawn from the parameters (the static list is empty). Set `collectionName` (the class). FILTER ON THE SERVER with `visualFilter`: {\"combinator\": \"and\", \"rules\": [{\"property\": \"<field>\", \"operator\": \"equal to\", \"input\": \"<name>\"}]} — each rule's `input` mints an input port `qp-<name>`; wire the value into it (e.g. a User node's `id` → `qp-ownerId`), or give the rule a literal `\"value\"` instead of `input`. Operators: \"equal to\", \"not equal to\", \"greater than\", \"greater than or equal to\", \"less than\", \"less than or equal to\", \"contain\" (case-insensitive), \"exist\", \"not exist\" (no value), \"points to\" (a pointer field equals a record id), and \"related to\" (with `relatedTo` and `relationProperty`). A rule may itself be a {\"combinator\": \"or\", \"rules\": [...]} group. The filter is compiled to the backend's `where`, so only matching rows leave the server. Sort: `visualSort`: [{\"property\": \"<field>\", \"order\": \"ascending\" | \"descending\"}]. Paging: `storageEnableLimit`: true with `storageLimit` and `storageSkip` (both ignored without it); `storageEnableCount`: true adds the `storageTotalCount` output. `storageFetch` (Do) is the signal that re-runs it. ⚠️ A rule whose `qp-` value has not arrived yet is DROPPED, not failed, so the query that runs when the graph is built returns every row the user may read: with a wired filter value set `\"runOnChange-collectionName\": false` and `\"runOnChange-querySettings\": false` and leave Do unwired, so the value's arrival is what runs the query (get_example cloud-query-own-rows). Any other parameter name (`querySettings`, `filter`, `where`…) is stored and never read, and the write is refused. Once the filter is opened in the editor's panel it is saved as {\"type\": \"and\", \"conditions\": [{\"field\", \"operator\": \"equalTo\", \"valueSource\": \"connected\", \"valuePortName\": \"qp-<name>\"}]}; the runtime reads both shapes.",
8061
8059
  "examples": [
8060
+ "cloud-query-own-rows",
8062
8061
  "repeater-query-records",
8063
8062
  "send-event-across-components",
8064
8063
  "cloud-record-live-refresh",
@@ -8068,12 +8067,14 @@
8068
8067
  "community-email-signup-validat"
8069
8068
  ],
8070
8069
  "patterns": [
8070
+ "A signed-in user's own rows: `visualFilter` rule {\"property\": \"owner\", \"operator\": \"equal to\", \"input\": \"owner\"}, the User node's `id` wired into `qp-owner`, and both fetch-at-build boxes off — the server answers with only that user's rows. `owner` is the backend's: it writes the creator into every record from their session, and a page never sends it. A field the page filled from the User id (an ownerId, a familyId) is the caller's word, never proof of whose a record is.",
8071
8071
  "`items` → Repeater `items`, with record properties delivered to the item component via Component Inputs: the canonical list page.",
8072
8072
  "Event Receiver `eventReceived` → fetch input: re-run a query on demand from anywhere in the app.",
8073
8073
  "Turn on Subscribe To Changes and wire `Changed` back into this node's own `Do`: the list stays current without polling, on every backend whose descriptor allows it — SSE on the built-in backend and PocketBase, a WebSocket on Directus.",
8074
8074
  "`Subscribed` → a status indicator's visibility, so people can see when live updates are actually running rather than assuming."
8075
8075
  ],
8076
8076
  "antiPatterns": [
8077
+ "Fetching every row and dropping the ones a user should not see in a Function or Filter Records: every row still reaches every browser. Filter on the server with `visualFilter` and a `qp-` port.",
8077
8078
  "Re-querying on a timer to feel 'live' — results already update from cloud events; explicit re-fetch is only needed when the server changed outside those events.",
8078
8079
  "Treating `Changed Record` as the full record on a delete — delete events carry only primary keys, so it is null and only `Changed Record Id` is populated.",
8079
8080
  "Polling with a Timer while Subscribe To Changes is available on this backend. Subscribe and re-fetch on `Changed` instead.",
@@ -8539,7 +8540,7 @@
8539
8540
  "name": "signal"
8540
8541
  },
8541
8542
  "isSignal": true,
8542
- "description": "Fires when the Id binds a record, and again when a Fetch finishes reading it — a bind has read nothing, so use Done to act on data that is really there"
8543
+ "description": "Fires when the Id binds a record already in memory, when a record the Id names is first read from the backend, and when a Fetch finishes reading it; use Done to act on the answer to a Fetch"
8543
8544
  },
8544
8545
  {
8545
8546
  "name": "id",
@@ -8632,14 +8633,14 @@
8632
8633
  "enrichment": {
8633
8634
  "typeName": "DbModel2",
8634
8635
  "summary": "Record: binds to one cloud database record by id and exposes each of its properties as an output that follows the record.",
8635
- "description": "DbModel2 (Record) points at a single record of a cloud database class and exposes one value output per property of that class (registered as `prop-<property>`), plus a `changed-<property>` signal per property. Setting `modelId` binds the node to the record in the app's local record store — properties already known locally (for example from a Query Records result) appear immediately, and `fetched` fires. Triggering `fetch` (Fetch, signal) loads the record fresh from the server by id; while `fetch` is connected, local change events no longer drive the outputs, so updates arrive only when you fetch. `changed` fires whenever the bound record changes from anywhere in the app (a Update Record elsewhere, a live query update). `fetched` and `changed` are value-level announcements and fire on binding as well as on a fetch; `done`/`failure`/`completed` are the outcome of a `fetch` invocation specifically, so wire those to sequence a chain and `fetched` to react to the data.",
8636
+ "description": "DbModel2 (Record) points at a single record of a cloud database class and exposes one value output per property of that class (registered as `prop-<property>`), plus a `changed-<property>` signal per property. Setting `modelId` binds the node to that record. A record already in memory (for example from a Query Records result on the page you came from) appears at once and `fetched` fires. A record nothing in memory holds yet (a page opened by its own URL: a reload, a bookmark, a shared link) is read from the backend first, and `fetched` fires when it arrives; until then the property outputs are empty. Triggering `fetch` (Fetch, signal) re-reads the record from the server by id. `changed` fires whenever the bound record changes from anywhere in the app (an Update Record elsewhere, a live query update). `fetched` and `changed` are value-level announcements; `done`/`failure`/`completed` are the outcome of a `fetch` invocation specifically, so wire those to sequence a chain and `fetched` to react to the data. A read the bind makes on its own reports a failure on `error`, not on `failure`.",
8636
8637
  "whenToUse": "Use it to read one record's properties — a detail page, or the current item inside a repeater (set `idSource` to 'From repeater'). It does not write: pair it with SetDbModelProperties to save edits. For lists of records use DbCollection2 (Query Records); for records from a non-cloud (BYOB) backend use noodl.byob.QueryData, whose results are plain objects, not records.",
8637
8638
  "ports": {
8638
8639
  "modelId": "Value input (connections only): the record id to bind to; a full record object is also accepted and dereferenced to its id.",
8639
8640
  "idSource": "Edit-only choice: 'Specify explicitly' uses `modelId`; 'From repeater' binds to the record driving the nearest enclosing For Each item.",
8640
- "fetch": "Signal; fetches the record from the server by the current `modelId`. Connecting it also switches the node to fetch-driven mode: outputs update on Fetch instead of on local record changes.",
8641
- "fetched": "Signal; fires after the record resolves — on bind when `fetch` is not connected, or after a server fetch completes.",
8642
- "changed": "Signal; fires when the bound record's data changes anywhere in the app (suppressed while `fetch` is connected).",
8641
+ "fetch": "Signal; re-reads the record from the server by the current `modelId`, replacing the copy held in memory. Not needed for a page opened by its own URL: binding an id nothing in memory holds already reads it.",
8642
+ "fetched": "Signal; fires when the record is there to read: on bind for a record already in memory, when the backend answers for one that was not, and after a Fetch completes.",
8643
+ "changed": "Signal; fires when the bound record's data changes anywhere in the app.",
8643
8644
  "id": "String value; the id of the currently bound record."
8644
8645
  },
8645
8646
  "runtimeBehavior": "Property ports are runtime-determined from the cloud database class selected via the `collectionName` parameter (itself a runtime-registered enum of the project's classes plus _User and _Role): each schema property becomes a value output `prop-<name>` (typed from the schema) and a signal output `changed-<name>`. Relation properties get no ports. An authoring tool should set `collectionName` in parameters and may reference `prop-<name>`/`changed-<name>` for properties it knows exist in the class schema.",
@@ -8651,7 +8652,7 @@
8651
8652
  "`prop-<name>` → a Text Input's `startValue`, edited text → Update Record `prop-<name>`: prefill an edit form and save it back."
8652
8653
  ],
8653
8654
  "antiPatterns": [
8654
- "Connecting `fetch` just to 'refresh' — while `fetch` is connected the node stops following local record changes, so only do it when you explicitly want server-fetch-driven updates.",
8655
+ "Reading the property outputs before `fetched` on a page that can be opened by its own URL — until the backend answers they are empty, and an Update Record fed from them then writes the empties back. Act on `fetched` (or a Fetch's `done`).",
8655
8656
  "Comparing a read-back Object field with `JSON.stringify` — an Object field does not keep the order of its names (a backend on PostgreSQL gives them back with shorter names first), so two equal objects can give different text. Compare them name by name, and sort the names yourself where a page shows them in an order."
8656
8657
  ],
8657
8658
  "relatedNodes": [
@@ -21824,7 +21825,8 @@
21824
21825
  "`done` → a navigation or Event Sender, with `id` as payload: jump straight to the new record's detail page."
21825
21826
  ],
21826
21827
  "antiPatterns": [
21827
- "Re-fetching a Query Records node after `done` — live queries already receive the new record; only re-fetch when the query is not live."
21828
+ "Re-fetching a Query Records node after `done` — live queries already receive the new record; only re-fetch when the query is not live.",
21829
+ "Wiring the User node's `id` into a field of your own (an ownerId, a familyId, a teamId) to say whose the record is. The backend already writes the creator into `owner` from the session and drops an `owner` the page sends; a field the page fills is whatever the caller sent, so anyone signed in can name another id. Read `owner` wherever it is decided who sees the record (example `cloud-share-my-records-with-someone`)."
21828
21830
  ],
21829
21831
  "relatedNodes": [
21830
21832
  "SetDbModelProperties",
@@ -31286,6 +31288,17 @@
31286
31288
  "description": "Read and write rules stored on the record as it is written, each rule adding its own Target, Read and Write ports; the NodeGX backend enforces them on every query and every realtime event, and backends with no per-record access control ignore them",
31287
31289
  "index": 1000
31288
31290
  },
31291
+ {
31292
+ "name": "addTo",
31293
+ "displayName": "Add To",
31294
+ "group": "General",
31295
+ "plug": "input",
31296
+ "type": {
31297
+ "name": "string"
31298
+ },
31299
+ "isSignal": false,
31300
+ "description": "Property names, comma-separated (e.g. points). For these, the value wired in is an amount added to what the backend holds, in one step on the backend, instead of replacing it: two devices adding at once both count. Wire the amount (1, -1, 5). The record then shows the backend's total. Leave empty to write every value as given."
31301
+ },
31289
31302
  {
31290
31303
  "name": "idSource",
31291
31304
  "displayName": "Id Source",
@@ -31575,6 +31588,9 @@
31575
31588
  "onlyIfUnchanged": [
31576
31589
  "always: read by the next save — the fields the backend must still hold as this record holds them, or the save is refused"
31577
31590
  ],
31591
+ "addTo": [
31592
+ "always: read by the next Do — the fields whose value is an amount the backend adds, not a value it stores"
31593
+ ],
31578
31594
  "accessControl": [
31579
31595
  "always: read by the next call — the access rules sent with the record"
31580
31596
  ],
@@ -31588,7 +31604,7 @@
31588
31604
  "typeName": "SetDbModelProperties",
31589
31605
  "summary": "Update Record: writes property values to an existing cloud database record when Do is triggered.",
31590
31606
  "description": "SetDbModelProperties (Update Record) updates the record identified by `modelId` in the class chosen with the `collectionName` (Class) parameter. Each class property becomes a value input; triggering `store` (Do) applies the connected inputs to the local record and saves them to the server, firing `done` (Done) on confirmation or `failure` with `error` on problems, and `completed` after either. `storeType` (Store to) can switch the node to 'Local only', which updates the in-memory record without any server call — useful for optimistic UI or scratch state. By default only the properties you actually supply are sent (`storeProperties` = 'Only specified'); 'All' sends the record's entire current data. Because the shared local record is updated, every bound Record node and live query in the app sees the change immediately.",
31591
- "whenToUse": "The save half of every edit form, and the way to flip a single field ('mark done') from a list row. To create a record use NewDbModelProperties; to remove one use DeleteDbModelProperties; for BYOB backends use noodl.byob.UpdateRecord.",
31607
+ "whenToUse": "The save half of every edit form, and the way to flip a single field ('mark done') from a list row. To create a record use NewDbModelProperties; to remove one use DeleteDbModelProperties; for BYOB backends use noodl.byob.UpdateRecord. For a count, score, balance or progress, set `addTo` and wire the change, not the new total.",
31592
31608
  "ports": {
31593
31609
  "store": "Signal (displayed as Do); saves to cloud (default) or applies locally, depending on `storeType`.",
31594
31610
  "done": "Signal (displayed as Done); fires after the server confirms the save, or immediately after a local-only store.",
@@ -31598,21 +31614,26 @@
31598
31614
  "storeProperties": "Edit-only choice ('Only specified' / 'All'): whether to send just the supplied property inputs or the record's whole data. Shown while `storeType` is cloud.",
31599
31615
  "storeType": "Edit-only choice ('Cloud and local' / 'Local only'): whether the save reaches the server or only the in-memory record.",
31600
31616
  "accessControl": "Edit-only list of access rules applied with the save; each rule expands into its own Target/Read/Write inputs.",
31601
- "onlyIfUnchanged": "Edit-only text (displayed as Only If Unchanged): property names, comma-separated, usually `version`. The update applies only if those properties still hold the values the record had when it was read (taken before this node's inputs are applied), checked by the backend inside the UPDATE itself. If someone else wrote the record in between, nothing is written, `failure` fires, `error` says so, and the local record goes back to what it held (the backend answered 409 with reason `precondition-failed`). To retry, FETCH the record again first: retrying with the same values fails the same way. NodeGX backends only; other backends refuse it. Empty: always write."
31617
+ "onlyIfUnchanged": "Edit-only text (displayed as Only If Unchanged): property names, comma-separated, usually `version`. The update applies only if those properties still hold the values the record had when it was read (taken before this node's inputs are applied), checked by the backend inside the UPDATE itself. If someone else wrote the record in between, nothing is written, `failure` fires, `error` says so, and the local record goes back to what it held (the backend answered 409 with reason `precondition-failed`). To retry, FETCH the record again first: retrying with the same values fails the same way. NodeGX backends only; other backends refuse it. Empty: always write.",
31618
+ "addTo": "Edit-only text (displayed as Add To): property names, comma-separated. For these, the value wired into `prop-<name>` is an AMOUNT (1, -1, 5) the backend adds to what it holds, in one step, instead of a value that replaces it, so two devices or tabs adding at once both count. The record then shows the backend's total. The other properties are saved first, so `onlyIfUnchanged` still guards them and a refused save adds nothing. A value that is not a number fails the Do and says so. 'Local only' adds the amount to the record held in memory."
31602
31619
  },
31603
31620
  "runtimeBehavior": "Ports are runtime-determined: `collectionName` (Class) is registered as an edit-only enum of the project's classes plus _User/_Role, and each schema property of the chosen class becomes a value input `prop-<name>` typed from the schema. Access Control rules spawn `acl-<ruleId>-*` inputs like on Create Record. An authoring tool should set `collectionName` in parameters and drive the `prop-<name>` inputs it intends to change.",
31604
31621
  "examples": [
31605
31622
  "cloud-edit-record",
31606
- "cloud-record-crud"
31623
+ "cloud-record-crud",
31624
+ "cloud-shared-counter"
31607
31625
  ],
31608
31626
  "patterns": [
31627
+ "A count, score, balance or progress: `addTo` = `points`, `prop-points` = 1 (or the change a Function works out), Button `onClick` → `store`. No script, no read of the old value: the backend adds it, and two devices pressing together both count (example `cloud-shared-counter`).",
31609
31628
  "DbModel2 `prop-<name>` → Text Input `startValue`, edited `text` → `prop-<name>`, Button `onClick` → `store`: prefill, edit, save.",
31610
31629
  "'From repeater' `idSource` inside a list row: one node updates whichever record the row represents.",
31611
31630
  "Two people editing one record: bump a `version` property on every save and set `onlyIfUnchanged` to `version`. The second save to land gets `failure` instead of silently replacing the first; on `failure`, fetch the record again (DbModel2) and let the person re-apply their change."
31612
31631
  ],
31613
31632
  "antiPatterns": [
31614
31633
  "Setting `storeProperties` to 'All' to 'be safe' — it uploads the record's entire local data and can overwrite fields other users changed; send only what the form edits.",
31615
- "Retrying a `failure` from `onlyIfUnchanged` by pressing Do again without fetching the record first: the precondition is taken from the record as it was read, so the retry fails the same way until the record is re-read."
31634
+ "Retrying a `failure` from `onlyIfUnchanged` by pressing Do again without fetching the record first: the precondition is taken from the record as it was read, so the retry fails the same way until the record is re-read.",
31635
+ "Saving a count, score, balance or progress that several tabs or devices change from the value the page holds (read it, add one in a Function, wire the new total into `prop-<name>`, Do): of two such saves the later erases the earlier. Set `addTo` to that property and wire the CHANGE (1, -1) instead: one node, no script (example `cloud-shared-counter`). For a value that is not a count, guard the whole-value save with `onlyIfUnchanged`.",
31636
+ "Wiring two controls (a Button's Click and a Text Input's On Enter) to two DIFFERENT nodes that each lead to this Do. A press that starts a save is taken once (the control's `repeatWhileSaving` is off by default), and controls wired to the SAME node share that guard; wired to different nodes, one press of each still saves twice. Wire both into the same node (the same Function's Run, or this Do)."
31616
31637
  ],
31617
31638
  "relatedNodes": [
31618
31639
  "DbModel2",
@@ -50480,6 +50501,18 @@
50480
50501
  "description": "How the element is placed: In Layout follows its siblings, Absolute ignores them, Sticky pins to the parent edge on overflow, Fixed stays put and takes no space",
50481
50502
  "index": 11
50482
50503
  },
50504
+ {
50505
+ "name": "repeatWhileSaving",
50506
+ "displayName": "Repeat While Saving",
50507
+ "group": "General",
50508
+ "plug": "input",
50509
+ "type": {
50510
+ "name": "boolean"
50511
+ },
50512
+ "isSignal": false,
50513
+ "default": false,
50514
+ "description": "Off (the default): when a click starts a save, the next click is ignored until the backend has answered, and for at least half a second, so a double press saves once. Controls wired to the same node share this. On: every click fires, for a control that saves each quick tap"
50515
+ },
50483
50516
  {
50484
50517
  "name": "scrollIntoView",
50485
50518
  "displayName": "Scroll Into View",
@@ -51279,9 +51312,10 @@
51279
51312
  "ports": {
51280
51313
  "label": "The button caption string.",
51281
51314
  "onClick": "Signal; the click event — wire this to the action (Condition `eval`, Navigate `navigate`, Send Event `sendEvent`, …).",
51282
- "enabled": "Boolean level input; false renders the disabled state and suppresses `onClick`. The current value is also mirrored as an output of the same name."
51315
+ "enabled": "Boolean level input; false renders the disabled state and suppresses `onClick`. The current value is also mirrored as an output of the same name.",
51316
+ "repeatWhileSaving": "Boolean (displayed as Repeat While Saving), default off. Off: when a click starts a backend save (a Record node's Do, a `Noodl.Records` call made before its first await), the next `onClick` from this control, or from any control wired to the same node, is ignored until the backend has answered and for at least half a second, so a double press saves once. A press that starts no save is never held. On: every `onClick` fires (a stepper that saves each tap)."
51283
51317
  },
51284
- "runtimeBehavior": "Declared-port-groups: icon ports appear when an icon source is chosen, and each visual state (hover, pressed, disabled, focused) contributes state-scoped styling ports. The catalog list is the superset; the editor shows the subset matching current parameters.",
51318
+ "runtimeBehavior": "Declared-port-groups: icon ports appear when an icon source is chosen, and each visual state (hover, pressed, disabled, focused) contributes state-scoped styling ports. The catalog list is the superset; the editor shows the subset matching current parameters. A `onClick` that starts a backend save is taken once: a second one is ignored while that save is waiting and for half a second after the press (`repeatWhileSaving` turns this off). There is no need to wire a busy flag back to `enabled` for a double press.",
51285
51319
  "examples": [
51286
51320
  "validate-before-submit",
51287
51321
  "navigate-on-click",
@@ -62677,6 +62711,18 @@
62677
62711
  "description": "How the element is placed: In Layout follows its siblings, Absolute ignores them, Sticky pins to the parent edge on overflow, Fixed stays put and takes no space",
62678
62712
  "index": 11
62679
62713
  },
62714
+ {
62715
+ "name": "repeatWhileSaving",
62716
+ "displayName": "Repeat While Saving",
62717
+ "group": "General",
62718
+ "plug": "input",
62719
+ "type": {
62720
+ "name": "boolean"
62721
+ },
62722
+ "isSignal": false,
62723
+ "default": false,
62724
+ "description": "Off (the default): when Enter starts a save, the next Enter is ignored until the backend has answered, and for at least half a second, so a double press saves once. Controls wired to the same node share this. On: every Enter fires, for a control that saves each quick tap"
62725
+ },
62680
62726
  {
62681
62727
  "name": "runOnChange-startValue",
62682
62728
  "displayName": "Value",
@@ -63650,9 +63696,10 @@
63650
63696
  "set": "Signal; applies the pending `startValue` to the field.",
63651
63697
  "clear": "Signal; empties the field.",
63652
63698
  "focus": "Signal; gives the field keyboard focus (`blur` removes it).",
63653
- "type": "Enum: text, textArea, email, number, password or url. textArea renders a multi-line editor."
63699
+ "type": "Enum: text, textArea, email, number, password or url. textArea renders a multi-line editor.",
63700
+ "repeatWhileSaving": "Boolean (displayed as Repeat While Saving), default off. Off: when Enter starts a backend save (a Record node's Do, a `Noodl.Records` call made before its first await), the next `onEnter` from this control, or from any control wired to the same node, is ignored until the backend has answered and for at least half a second, so a double press saves once. A press that starts no save is never held. On: every `onEnter` fires (a stepper that saves each tap)."
63654
63701
  },
63655
- "runtimeBehavior": "Declared-port-groups: `width`/`height` appear per `sizeMode`, icon ports per `useIcon`/`iconSourceType`, label styling ports when `useLabel` is true, and border/shadow ports per border-style and `boxShadowEnabled` settings. The catalog list is the superset; the editor shows the subset matching current parameters.",
63702
+ "runtimeBehavior": "Declared-port-groups: `width`/`height` appear per `sizeMode`, icon ports per `useIcon`/`iconSourceType`, label styling ports when `useLabel` is true, and border/shadow ports per border-style and `boxShadowEnabled` settings. The catalog list is the superset; the editor shows the subset matching current parameters. An `onEnter` that starts a backend save is taken once: a second one is ignored while that save is waiting and for half a second after the press (`repeatWhileSaving` turns this off). There is no need to wire a busy flag back to `enabled` for a double press.",
63656
63703
  "examples": [
63657
63704
  "ctl-settings-form",
63658
63705
  "community-email-signup-validat"
@@ -74299,6 +74346,135 @@
74299
74346
  }
74300
74347
  ]
74301
74348
  },
74349
+ {
74350
+ "id": "cloud-query-own-rows",
74351
+ "title": "Only my records: a Query Records filtered on the server by the signed-in user's id",
74352
+ "description": "A signed-in user's own rows, with the filter on the server (a server-side filter, the backend's where clause), not a Filter Records or a Function after the fetch. The Query Records node's `visualFilter` holds one rule, {\"property\": \"owner\", \"operator\": \"equal to\", \"input\": \"owner\"}, and that `input` mints the input port `qp-owner`; the User node's `id` is wired into it, so other users' rows never leave the server. `owner` is the backend's own field: it writes the creator's id into every record a signed-in user makes, from their session, and drops any `owner` a page sends, so the page that creates a Note sends no owner field at all. Never copy the user's id into a field of your own (an ownerId, a familyId, a teamId) to say whose a record is: that is the caller's word, and anyone signed in can send another id. Both fetch-at-build boxes are off (`runOnChange-collectionName` and `runOnChange-querySettings`: false) and Do is unwired: a rule whose qp- value has not arrived yet is dropped, so a query run before the id arrives would return every row the user may read; this way the id's arrival is what runs it. Sort and limit sit beside the filter (`visualSort`, `storageEnableLimit` + `storageLimit`). When a collection keeps its default creatorOwns rule and the backend enforces it, a user's query already returns only rows they own; the filter is how you narrow further, or how you ask for one person's rows in a shared collection.",
74353
+ "demonstrates": [
74354
+ "DbCollection2",
74355
+ "net.noodl.user.User",
74356
+ "For Each",
74357
+ "Component Inputs"
74358
+ ],
74359
+ "components": [
74360
+ {
74361
+ "name": "/My Notes",
74362
+ "nodes": [
74363
+ {
74364
+ "id": "notes_root",
74365
+ "type": "Group",
74366
+ "label": "My notes",
74367
+ "parameters": {
74368
+ "flexDirection": "column"
74369
+ },
74370
+ "children": [
74371
+ "notes_list"
74372
+ ]
74373
+ },
74374
+ {
74375
+ "id": "notes_list",
74376
+ "type": "For Each",
74377
+ "label": "One row per note of mine",
74378
+ "parent": "notes_root",
74379
+ "parameters": {
74380
+ "template": "/Note Row"
74381
+ }
74382
+ },
74383
+ {
74384
+ "id": "me",
74385
+ "type": "net.noodl.user.User",
74386
+ "label": "Signed-in user"
74387
+ },
74388
+ {
74389
+ "id": "my_notes",
74390
+ "type": "DbCollection2",
74391
+ "label": "Notes where owner = my id",
74392
+ "parameters": {
74393
+ "collectionName": "Note",
74394
+ "visualFilter": {
74395
+ "combinator": "and",
74396
+ "rules": [
74397
+ {
74398
+ "property": "owner",
74399
+ "operator": "equal to",
74400
+ "input": "owner"
74401
+ }
74402
+ ]
74403
+ },
74404
+ "visualSort": [
74405
+ {
74406
+ "property": "createdAt",
74407
+ "order": "descending"
74408
+ }
74409
+ ],
74410
+ "storageEnableLimit": true,
74411
+ "storageLimit": 50,
74412
+ "runOnChange-collectionName": false,
74413
+ "runOnChange-querySettings": false
74414
+ }
74415
+ }
74416
+ ],
74417
+ "connections": [
74418
+ {
74419
+ "fromId": "me",
74420
+ "fromProperty": "id",
74421
+ "toId": "my_notes",
74422
+ "toProperty": "qp-owner"
74423
+ },
74424
+ {
74425
+ "fromId": "my_notes",
74426
+ "fromProperty": "items",
74427
+ "toId": "notes_list",
74428
+ "toProperty": "items"
74429
+ }
74430
+ ]
74431
+ },
74432
+ {
74433
+ "name": "/Note Row",
74434
+ "nodes": [
74435
+ {
74436
+ "id": "row_root",
74437
+ "type": "Group",
74438
+ "label": "Note row",
74439
+ "parameters": {
74440
+ "flexDirection": "column",
74441
+ "paddingTop": "var(--space-2)",
74442
+ "paddingBottom": "var(--space-2)"
74443
+ },
74444
+ "children": [
74445
+ "row_title"
74446
+ ]
74447
+ },
74448
+ {
74449
+ "id": "row_title",
74450
+ "type": "Text",
74451
+ "label": "Note title",
74452
+ "parent": "row_root"
74453
+ },
74454
+ {
74455
+ "id": "row_inputs",
74456
+ "type": "Component Inputs",
74457
+ "label": "Record properties in",
74458
+ "ports": [
74459
+ {
74460
+ "name": "title",
74461
+ "plug": "output",
74462
+ "type": "*"
74463
+ }
74464
+ ]
74465
+ }
74466
+ ],
74467
+ "connections": [
74468
+ {
74469
+ "fromId": "row_inputs",
74470
+ "fromProperty": "title",
74471
+ "toId": "row_title",
74472
+ "toProperty": "text"
74473
+ }
74474
+ ]
74475
+ }
74476
+ ]
74477
+ },
74302
74478
  {
74303
74479
  "id": "cloud-record-crud",
74304
74480
  "title": "Records on a REST backend: query, create, update, delete",
@@ -74983,6 +75159,424 @@
74983
75159
  }
74984
75160
  ]
74985
75161
  },
75162
+ {
75163
+ "id": "cloud-share-my-records-with-someone",
75164
+ "title": "Invite someone to see my records: a share row, and a cloud function that trusts its owner",
75165
+ "description": "Letting another person see your records (invite a teacher, a coach, a family member, a colleague by email). The page creates a Share record holding only the address it is shared with: it sends no field naming whose records these are, because the backend already writes that into the record's `owner`, from the signed-in caller's own session, and drops any `owner` a page sends. A cloud function answers \"what has been shared with me\": the caller's address comes from a User node inside the function (the caller's session, not the request body), the shares that name that address say WHO shared (their `owner`), and the notes returned are exactly the notes those people own (`note.owner`). Never decide it by a field the page wrote (a familyId, a teamId, an ownerId set from the User id): anyone signed in can create a Share saying any id they like, and only `owner` is the server's word. The records stay creator-owned (the default rules), so the function, which reads as the system, is the only door to another person's notes, and it answers read-only. The address match is done in the function rather than as a server filter on purpose: a filter rule whose value has not arrived yet is dropped, which would widen a system read to every share.",
75166
+ "demonstrates": [
75167
+ "NewDbModelProperties",
75168
+ "noodl.cloud.request",
75169
+ "net.noodl.user.User",
75170
+ "DbCollection2",
75171
+ "JavaScriptFunction",
75172
+ "noodl.cloud.response",
75173
+ "CloudFunction2"
75174
+ ],
75175
+ "components": [
75176
+ {
75177
+ "name": "/Share My Notes",
75178
+ "nodes": [
75179
+ {
75180
+ "id": "share_root",
75181
+ "type": "Group",
75182
+ "label": "Share my notes",
75183
+ "parameters": {
75184
+ "flexDirection": "column",
75185
+ "rowGap": "var(--space-3)"
75186
+ },
75187
+ "children": [
75188
+ "share_address",
75189
+ "share_button",
75190
+ "share_error"
75191
+ ]
75192
+ },
75193
+ {
75194
+ "id": "share_address",
75195
+ "type": "net.noodl.controls.textinput",
75196
+ "label": "Their email address",
75197
+ "parent": "share_root",
75198
+ "parameters": {
75199
+ "type": "email",
75200
+ "placeholder": "name@example.com"
75201
+ }
75202
+ },
75203
+ {
75204
+ "id": "share_button",
75205
+ "type": "net.noodl.controls.button",
75206
+ "label": "Share with them",
75207
+ "parent": "share_root",
75208
+ "parameters": {
75209
+ "label": "Share my notes"
75210
+ }
75211
+ },
75212
+ {
75213
+ "id": "share_error",
75214
+ "type": "Text",
75215
+ "label": "Why it did not save",
75216
+ "parent": "share_root",
75217
+ "parameters": {
75218
+ "text": ""
75219
+ }
75220
+ },
75221
+ {
75222
+ "id": "share_create",
75223
+ "type": "NewDbModelProperties",
75224
+ "label": "A Share naming only their address (owner is the backend's)",
75225
+ "parameters": {
75226
+ "collectionName": "Share"
75227
+ }
75228
+ }
75229
+ ],
75230
+ "connections": [
75231
+ {
75232
+ "fromId": "share_address",
75233
+ "fromProperty": "onTextChanged",
75234
+ "toId": "share_create",
75235
+ "toProperty": "prop-withEmail"
75236
+ },
75237
+ {
75238
+ "fromId": "share_button",
75239
+ "fromProperty": "onClick",
75240
+ "toId": "share_create",
75241
+ "toProperty": "store"
75242
+ },
75243
+ {
75244
+ "fromId": "share_create",
75245
+ "fromProperty": "error",
75246
+ "toId": "share_error",
75247
+ "toProperty": "text"
75248
+ }
75249
+ ]
75250
+ },
75251
+ {
75252
+ "name": "/Shared With Me",
75253
+ "nodes": [
75254
+ {
75255
+ "id": "seen_root",
75256
+ "type": "Group",
75257
+ "label": "Notes shared with me",
75258
+ "parameters": {
75259
+ "flexDirection": "column",
75260
+ "rowGap": "var(--space-2)"
75261
+ },
75262
+ "children": [
75263
+ "seen_refresh",
75264
+ "seen_list"
75265
+ ]
75266
+ },
75267
+ {
75268
+ "id": "seen_refresh",
75269
+ "type": "net.noodl.controls.button",
75270
+ "label": "Refresh",
75271
+ "parent": "seen_root",
75272
+ "parameters": {
75273
+ "label": "Refresh"
75274
+ }
75275
+ },
75276
+ {
75277
+ "id": "seen_list",
75278
+ "type": "For Each",
75279
+ "label": "One row per shared note",
75280
+ "parent": "seen_root",
75281
+ "parameters": {
75282
+ "template": "/Shared Note Row"
75283
+ }
75284
+ },
75285
+ {
75286
+ "id": "seen_call",
75287
+ "type": "CloudFunction2",
75288
+ "label": "Ask the server what was shared with me",
75289
+ "parameters": {
75290
+ "function": "Cloud Functions/Shared With Me"
75291
+ }
75292
+ }
75293
+ ],
75294
+ "connections": [
75295
+ {
75296
+ "fromId": "seen_refresh",
75297
+ "fromProperty": "onClick",
75298
+ "toId": "seen_call",
75299
+ "toProperty": "call"
75300
+ },
75301
+ {
75302
+ "fromId": "seen_call",
75303
+ "fromProperty": "out-notes",
75304
+ "toId": "seen_list",
75305
+ "toProperty": "items"
75306
+ }
75307
+ ]
75308
+ },
75309
+ {
75310
+ "name": "/Shared Note Row",
75311
+ "nodes": [
75312
+ {
75313
+ "id": "row_root",
75314
+ "type": "Group",
75315
+ "label": "Shared note row",
75316
+ "parameters": {
75317
+ "flexDirection": "column",
75318
+ "paddingTop": "var(--space-2)",
75319
+ "paddingBottom": "var(--space-2)"
75320
+ },
75321
+ "children": [
75322
+ "row_title"
75323
+ ]
75324
+ },
75325
+ {
75326
+ "id": "row_title",
75327
+ "type": "Text",
75328
+ "label": "Note title",
75329
+ "parent": "row_root"
75330
+ },
75331
+ {
75332
+ "id": "row_inputs",
75333
+ "type": "Component Inputs",
75334
+ "label": "Note in",
75335
+ "ports": [
75336
+ {
75337
+ "name": "title",
75338
+ "plug": "output",
75339
+ "type": "*"
75340
+ }
75341
+ ]
75342
+ }
75343
+ ],
75344
+ "connections": [
75345
+ {
75346
+ "fromId": "row_inputs",
75347
+ "fromProperty": "title",
75348
+ "toId": "row_title",
75349
+ "toProperty": "text"
75350
+ }
75351
+ ]
75352
+ },
75353
+ {
75354
+ "name": "/#__cloud__/Cloud Functions/Shared With Me",
75355
+ "nodes": [
75356
+ {
75357
+ "id": "fn_request",
75358
+ "type": "noodl.cloud.request",
75359
+ "label": "The caller (no parameters: nothing to forge)",
75360
+ "parameters": {
75361
+ "allowNoAuth": false
75362
+ }
75363
+ },
75364
+ {
75365
+ "id": "fn_me",
75366
+ "type": "net.noodl.user.User",
75367
+ "label": "The caller's own session"
75368
+ },
75369
+ {
75370
+ "id": "fn_shares",
75371
+ "type": "DbCollection2",
75372
+ "label": "Every share",
75373
+ "parameters": {
75374
+ "collectionName": "Share",
75375
+ "runOnChange-collectionName": false,
75376
+ "runOnChange-querySettings": false
75377
+ }
75378
+ },
75379
+ {
75380
+ "id": "fn_notes",
75381
+ "type": "DbCollection2",
75382
+ "label": "Every note",
75383
+ "parameters": {
75384
+ "collectionName": "Note",
75385
+ "runOnChange-collectionName": false,
75386
+ "runOnChange-querySettings": false
75387
+ }
75388
+ },
75389
+ {
75390
+ "id": "fn_pick",
75391
+ "type": "JavaScriptFunction",
75392
+ "label": "The notes of the people who shared with this address",
75393
+ "parameters": {
75394
+ "functionScript": "if (Inputs.email === undefined || Inputs.shares === undefined || Inputs.notes === undefined) return;\nconst me = String(Inputs.email || '').trim().toLowerCase();\n// WHO shared is each share's owner: the backend wrote it from their session and nobody can change it.\n// withEmail is only what they typed. A field the page wrote (a groupId, an ownerId) proves nothing.\nconst from = new Set(\n Inputs.shares\n .filter((s) => me !== '' && String(s.withEmail || '').trim().toLowerCase() === me)\n .map((s) => s.owner)\n);\nOutputs.notes = Inputs.notes\n .filter((n) => from.has(n.owner))\n .map((n) => ({ id: n.objectId || n.id, title: n.title || '' }));\nOutputs.answer();"
75395
+ }
75396
+ },
75397
+ {
75398
+ "id": "fn_response",
75399
+ "type": "noodl.cloud.response",
75400
+ "label": "What this caller may see",
75401
+ "parameters": {
75402
+ "params": "notes"
75403
+ }
75404
+ },
75405
+ {
75406
+ "id": "fn_fail",
75407
+ "type": "noodl.cloud.response",
75408
+ "label": "Tell the caller it went wrong",
75409
+ "parameters": {
75410
+ "status": "failure"
75411
+ }
75412
+ }
75413
+ ],
75414
+ "connections": [
75415
+ {
75416
+ "fromId": "fn_request",
75417
+ "fromProperty": "receive",
75418
+ "toId": "fn_me",
75419
+ "toProperty": "fetch"
75420
+ },
75421
+ {
75422
+ "fromId": "fn_request",
75423
+ "fromProperty": "receive",
75424
+ "toId": "fn_shares",
75425
+ "toProperty": "storageFetch"
75426
+ },
75427
+ {
75428
+ "fromId": "fn_request",
75429
+ "fromProperty": "receive",
75430
+ "toId": "fn_notes",
75431
+ "toProperty": "storageFetch"
75432
+ },
75433
+ {
75434
+ "fromId": "fn_me",
75435
+ "fromProperty": "email",
75436
+ "toId": "fn_pick",
75437
+ "toProperty": "in-email"
75438
+ },
75439
+ {
75440
+ "fromId": "fn_shares",
75441
+ "fromProperty": "items",
75442
+ "toId": "fn_pick",
75443
+ "toProperty": "in-shares"
75444
+ },
75445
+ {
75446
+ "fromId": "fn_notes",
75447
+ "fromProperty": "items",
75448
+ "toId": "fn_pick",
75449
+ "toProperty": "in-notes"
75450
+ },
75451
+ {
75452
+ "fromId": "fn_pick",
75453
+ "fromProperty": "out-notes",
75454
+ "toId": "fn_response",
75455
+ "toProperty": "pm-notes"
75456
+ },
75457
+ {
75458
+ "fromId": "fn_pick",
75459
+ "fromProperty": "out-answer",
75460
+ "toId": "fn_response",
75461
+ "toProperty": "send"
75462
+ },
75463
+ {
75464
+ "fromId": "fn_pick",
75465
+ "fromProperty": "error",
75466
+ "toId": "fn_fail",
75467
+ "toProperty": "errorMessage"
75468
+ },
75469
+ {
75470
+ "fromId": "fn_pick",
75471
+ "fromProperty": "failure",
75472
+ "toId": "fn_fail",
75473
+ "toProperty": "send"
75474
+ }
75475
+ ]
75476
+ }
75477
+ ]
75478
+ },
75479
+ {
75480
+ "id": "cloud-shared-counter",
75481
+ "title": "A count several devices change: Update Record adds on the server, each press taken once",
75482
+ "description": "A value two tabs or two devices change at once (a count, a score, a balance, progress) is changed on the server by the amount, never read into the page, changed, and written back whole: of two such whole saves the later erases the earlier. Here Update Record names `points` in Add To, so the 1 on `prop-points` is an amount the backend adds in one step, and the Record node bound to the same id shows the backend's total. No script, and no read of the old value. The press is taken once with no wiring: while the save a click started is waiting, and for half a second, the Button's next click is ignored (its Repeat While Saving is off). A Text Input's On Enter wired to the same Do shares that guard. To add a different amount, wire it into `prop-points` (a Function's output, -1 for a take-away button). To replace a whole value instead, use Update Record with `onlyIfUnchanged`. For a change that also needs `Noodl.Records.increment` in a script, read `recordId` once at the top so the answer belongs to the record the press was for.",
75483
+ "demonstrates": [
75484
+ "SetDbModelProperties",
75485
+ "DbModel2",
75486
+ "net.noodl.controls.button"
75487
+ ],
75488
+ "components": [
75489
+ {
75490
+ "name": "/Shared Count",
75491
+ "nodes": [
75492
+ {
75493
+ "id": "root",
75494
+ "type": "Group",
75495
+ "label": "Count",
75496
+ "parameters": {
75497
+ "flexDirection": "row",
75498
+ "alignItems": "center"
75499
+ },
75500
+ "children": [
75501
+ "count_text",
75502
+ "add_btn"
75503
+ ]
75504
+ },
75505
+ {
75506
+ "id": "count_text",
75507
+ "type": "Text",
75508
+ "label": "Total so far",
75509
+ "parent": "root"
75510
+ },
75511
+ {
75512
+ "id": "add_btn",
75513
+ "type": "net.noodl.controls.button",
75514
+ "label": "Add one",
75515
+ "parent": "root",
75516
+ "parameters": {
75517
+ "label": "+1"
75518
+ }
75519
+ },
75520
+ {
75521
+ "id": "count_inputs",
75522
+ "type": "Component Inputs",
75523
+ "label": "Record id in",
75524
+ "ports": [
75525
+ {
75526
+ "name": "recordId",
75527
+ "plug": "output",
75528
+ "type": "string"
75529
+ }
75530
+ ]
75531
+ },
75532
+ {
75533
+ "id": "count_record",
75534
+ "type": "DbModel2",
75535
+ "label": "The shared record",
75536
+ "parameters": {
75537
+ "collectionName": "Score"
75538
+ }
75539
+ },
75540
+ {
75541
+ "id": "add_one",
75542
+ "type": "SetDbModelProperties",
75543
+ "label": "Add one on the server",
75544
+ "parameters": {
75545
+ "collectionName": "Score",
75546
+ "addTo": "points",
75547
+ "prop-points": 1
75548
+ }
75549
+ }
75550
+ ],
75551
+ "connections": [
75552
+ {
75553
+ "fromId": "count_inputs",
75554
+ "fromProperty": "recordId",
75555
+ "toId": "count_record",
75556
+ "toProperty": "modelId"
75557
+ },
75558
+ {
75559
+ "fromId": "count_inputs",
75560
+ "fromProperty": "recordId",
75561
+ "toId": "add_one",
75562
+ "toProperty": "modelId"
75563
+ },
75564
+ {
75565
+ "fromId": "count_record",
75566
+ "fromProperty": "prop-points",
75567
+ "toId": "count_text",
75568
+ "toProperty": "text"
75569
+ },
75570
+ {
75571
+ "fromId": "add_btn",
75572
+ "fromProperty": "onClick",
75573
+ "toId": "add_one",
75574
+ "toProperty": "store"
75575
+ }
75576
+ ]
75577
+ }
75578
+ ]
75579
+ },
74986
75580
  {
74987
75581
  "id": "cloud-sign-a-token-with-a-stored-secret",
74988
75582
  "title": "Mint a signed token for another service to check",