Data Transfer ============= .. _data-transfer: CARE implements a **data subscription mechanism** between the backend and the frontend, enabling real-time collaboration and automatic synchronization of database tables with the Vuex store in the frontend application. .. _data-flow: Data Flow --------- .. figure:: ./data_flow.drawio.png :width: 100% :align: center :alt: CARE Data Flow: Component ↔ Sockets ↔ Vuex ↔ Computed ----- **Prerequisites** - **Model** sets ``autoTable = true`` to be mirrored into Vuex. - **Component** declares ``subscribeTable: [""]`` (or object form) to receive pushes. Step-by-step (Backend → Frontend) ~~~~~~~~~~~~~~~~~~~~~~ 1. **DB model (top-center “Database”)**: The model sets ``autoTable = true`` to enable automatic Vuex store table creation and participate in app data sync. 2. **Subscribe (bottom-left “Example Component” → center “Sockets”)**: On mount, the ``subscribeTable`` plugin emits ``subscribeAppData`` for each table to **Sockets (AppSocket, center)**; the server returns a ``subscriptionId``. 3. **Access-aware filtering (center “Sockets” ↔ top-center “Database”)**: Before sending any data, **Sockets** applies the model’s ``accessMap`` / ``publicTable`` via ``getFiltersAndAttributes`` so each socket only receives permitted rows/columns. 4. **Initial snapshot (center “Sockets” → bottom-right “Vuex Store”)**: The server calls ``sendTable`` and emits ``Refresh`` to the **Vuex Store (right)** with the current rows. The **same event name** is used later for updates. 5. **Socket write (optional; bottom-left “Example Component” → center “Sockets”)**: The frontend emits ``appDataUpdate`` with ``{ table, data }`` to create or update rows. 6. **Backend transaction (center “Sockets” → top-center “Database”)**: ``updateData`` validates, applies defaults, and enforces access; inside a transaction it persists changes to the **Database (top-center)**. On **commit** (``afterCommit``) the server aggregates changed rows of ``autoTable`` models. See also: :ref:`MetaModel behavior ` for the shared logic used in ``updateData``, including soft-deletion, access filtering, and hook handling. 7. **Broadcast (center “Sockets” → right “Vuex Store”)**: ``broadcastTable`` re-applies per-socket visibility with ``getFiltersAndAttributes`` (admins/public can bypass) and emits ``Refresh`` only to relevant subscribers; **Vuex (right)** receives the updated data. 8. **Vuex merge (right “Vuex Store”)**: Each autoTable module handles the ``Refresh`` mutation and merges rows (see ``refreshState``); ``refreshCount`` increments. 9. **Computed → Component (lower-left “Computed Getters” → bottom-left “Example Component”)**: Components read via Vuex getters (e.g., ``getAll``, ``getFiltered``); computed properties update and the **Component** re-renders automatically. .. tip:: **Two directions in the diagram** - **Downstream:** ``Refresh``: (Sockets → Vuex) is used for the **initial snapshot** and all **subsequent updates**. - **Upstream:** ``appDataUpdate``: (Component → Sockets) performs a write; after commit the server pushes ``Refresh`` to keep everyone consistent. **Unsubscribe (on unmount; bottom-left “Example Component”)** The component emits ``unsubscribeAppData(subscriptionId)``; the socket removes the subscription so no further ``Refresh`` events are delivered. sendTable ~~~~~~~~~ - **Purpose**: send the current snapshot for a table and emit updates to the client. - **Preconditions**: only works for models with ``autoTable = true``; otherwise it no-ops with a log. - **Filtering**: starts with ``{ deleted: false }`` and OR-adds any provided ``filter``; then applies per-socket access via ``getFiltersAndAttributes`` (may also add allowed columns). - **Attributes**: excludes sensitive fields by default (e.g., ``deleted``, ``deletedAt``, ``updatedAt``, ``rolesUpdatedAt``, ``initialPassword``, ``passwordHash``, ``salt``). - **Injects**: supports ``{ type: "count", table, by, as }``; counts related rows and injects the result into each entry as ``as``. - **Related tables**: if the model’s ``autoTable.foreignTables`` or ``autoTable.parentTables`` are set, it also fetches those and emits ``Refresh`` for them. - **Emit & return**: always emits ``Refresh`` with the rows and returns the same data. AppDataUpdate Socket -------------------- The ``appDataUpdate`` socket is the generic way to **create or update rows** in models with ``autoTable = true``. It is defined in ``backend/webserver/sockets/app.js`` inside the ``AppSocket`` class: .. code-block:: javascript /** * Update data for a specific table to the client. * * Acts as a wrapper around the underlying `updateData` method, using a Sequelize * transaction if provided, and returns the outcome to the caller. * * @socketEvent appDataUpdate * @param {Object} data The input data from the frontend * @param {String} data.table The name of the table to update * @param {Object} data.data New data to update * @param {Object} options Additional configuration parameter * @param {Object} options.transaction Sequelize DB transaction options * @returns {Promise<*>} A promise that resolves with the result from updateData */ async updateAppData(data, options) { return await this.updateData(data, options); } This method wraps the internal ``updateData`` logic. It checks access permissions, validates required fields, applies defaults, and then either **creates a new row** or **updates an existing one**. All operations run inside a transaction, and on commit the modified rows are broadcast to subscribed clients (see :ref:`data-flow`). When to use ~~~~~~~~~~~ - **Create** a new row in an autoTable model — omit ``id`` or set it to ``0``. - **Update** an existing row — include ``id`` and the fields to change. - **Soft-delete / quick state toggles** — include ``id`` with one of ``deleted``, ``closed``, ``public`` or ``end``; the server updates immediately. - **Not for reads** — data is delivered via ``Refresh`` after you subscribe with ``subscribeAppData``. - **Don’t use** for non-``autoTable`` models or bulk/admin maintenance — use dedicated endpoints instead. How to use ~~~~~~~~~~ From the frontend you call: .. code-block:: javascript this.$socket.emit("appDataUpdate", { table: "", // must be an autoTable model data: { /* fields */ } // include id for updates, omit for create }, (result) => { if (result.success) { // result.data contains the new or updated id } else { // result.message contains a human-readable error } }); .. tip:: - **Create**: omit ``id`` or set it to ``0`` - **Update**: include ``id`` - **Quick state toggles**: if you send an ``id`` with ``deleted``, ``closed``, ``public`` or ``end``, the server will update immediately without requiring other fields. .. reminder:: ``appDataUpdate`` only works for models with ``autoTable = true``. For the definition of models, see :doc:`Database `. **Example usage in the frontend** A typical usage pattern is emitting the socket event and showing a toast depending on the result. .. code-block:: javascript // Close a study (from Study.vue) this.$socket.emit("appDataUpdate", { table: "study", data: { id: studyId, closed: true } }, (result) => { if (result.success) { this.eventBus.emit('toast', { title: "Study closed", message: "The study has been closed", variant: "success" }); } else { this.eventBus.emit('toast', { title: "Study closing failed", message: result.message, variant: "danger" }); } }); .. important:: The server response has the shape: ``{ success: true, data: }`` on success, or ``{ success: false, message: "" }`` on failure. **Fast path:** If ``id`` is present and any of ``deleted``, ``closed``, ``public`` or ``end`` is included, the server updates the row immediately via ``updateById`` (skips required-field checks and nested recursion). ``closed: true`` is stored as a timestamp. After commit, the change is broadcast via ``Refresh`` as usual. Backend Update Logic ~~~~~~~~~~~~~~~~~~~~ Typical component actions map to small ``appDataUpdate`` calls: - **Delete** - soft-delete a row: .. code-block:: javascript this.$socket.emit("appDataUpdate", { table: "user", data: { id, deleted: true } }, cb); - **Convert to child item**: Create/update a **parent** and send **child** rows in an array field. The server detects ``fields`` of type ``table`` and **recursively** runs updates for each child, injecting the parent’s foreign key. - **Fast-path updates**: If an entry has an ``id`` and includes one of ``deleted``, ``closed``, ``public`` or ``end``, it is updated immediately without requiring other fields (schema required-field checks are skipped for these toggles). - **Timestamp semantics for** ``closed``: The backend stores ``closed`` as a **timestamp**. Passing ``{ closed: true }`` sets it to the current time (see ``MetaModel.updateById``). **Important details** - **Validation**: On create, required fields are checked and defaults are applied if not set through the frontend. - **Access control**: if ``userId`` is provided, the server verifies the caller is allowed to update for that user. - **Nested tables**: if the model defines fields of type ``table``, the server recursively calls ``updateData`` to update child rows. - **Broadcasts**: after a successful commit, all subscribed clients receive the updated data automatically. .. _table-refresh-events: Store Updates & ``
Refresh`` Events ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Sockets broadcast table changes via events named ``Refresh``. Each autoTable module registers a mutation with that exact name and merges incoming rows using ``refreshState``. **Mutation shape (generated per table):** .. code-block:: javascript [websocketPrefix + table.name + "Refresh"]: (state, data) => { // table-specific pre-processing (e.g., annotation/comment tweaks) may occur here refreshState(state, data, (table.name !== "tag")); state.refreshCount++; } **Merge behavior:** The helper function ``refreshState`` determines how incoming rows are handled: - Existing entries with the same ``id`` are **overwritten** with the new data. - Entries with ``deleted: true`` are **removed** from the Vuex store. .. code-block:: javascript export function refreshState(state, data, removeDeleted = true) { if (!Array.isArray(data)) data = [data]; data.map((entry) => { if (!entry.deleted) { state.data[entry.id] = { ...state.data[entry.id], ...entry }; } else if (removeDeleted) { delete state.data[entry.id]; } }); } .. tip:: If you don’t see updates, verify: 1) the model has ``autoTable = true``, 2) the component declared ``subscribeTable: ["
"]``, 3) the same ``Refresh`` mutation fires for the **initial snapshot** and later **updates** (check Vue devtools). Frontend access patterns and the list of available getters are documented in :doc:`Vuex Store <../frontend/vuex_store>`. For inspecting Vuex modules, socket messages, and mutations with browser devtools, see :doc:`Debugging <../debugging/index>`. Subscribe DB in the Frontend (when & where) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Use ``subscribeTable`` in route views or components that actively display changing table data; unsubscription happens automatically on unmount. On mount, the plugin checks for a component option ``subscribeTable`` and emits ``subscribeAppData`` for each entry; on unmount it emits ``unsubscribeAppData(subscriptionId)``. The server tracks subscriptions per socket in ``socket.appDataSubscriptions`` and uses them to decide which clients receive updates. .. code-block:: javascript // Minimal usage (string form) export default { subscribeTable: ["nav_element"] } .. code-block:: javascript // Object form supports optional filter/inject export default { subscribeTable: [ "nav_element", { table: "document", filter: [{ projectId: 42 }], inject: [{ type: "count", table: "comment", by: "documentId", as: "comments" }] } ] } Accessing the Vuex Store in Components -------------------------------------- Use the generated getters under the ``table/`` namespace directly in ``computed``: .. code-block:: javascript computed: { // all documents documents() { return this.$store.getters["table/document/getAll"]; }, // filtered by project projectDocuments() { const pid = this.$store.getters["settings/getValueAsInt"]("projects.default"); return this.$store.getters["table/document/getFiltered"](d => d.projectId === pid); }, // single by id doc() { return this.$store.getters["table/document/get"](this.$route.params.id); } } Available getters per autoTable module: - ``get(id)`` → object by id - ``getAll()`` → all rows - ``getFiltered(fn)`` → filter predicate - ``getByHash(hash)`` → first row by hash - ``getByUser(userId)`` → rows by userId - ``getByKey(key, value)`` → rows where ``row[key] === value`` - ``length`` → number of rows - ``refreshCount`` → number of times the table has been refreshed - ``hasFields`` / ``getFields`` → schema metadata if available Collaboration Use Case ~~~~~~~~~~~~~~~~~~~~~~ This subscription system enables **real-time collaboration**: - If one user modifies a row (e.g., adds an annotation), all other users subscribed to the same table immediately receive the updated data. - By maintaining a per-user subscription list, the backend ensures that **only relevant users** are notified about changes. This approach avoids polling and keeps all connected clients synchronized with the authoritative state in the database.