Files
Miguel de la CruzandMiguel de la Cruz 1eb4c62cf9 PSAv2 endpoint improvements (#36782)
* Updates search opts and cursor for searching property fields

* Adds store changes and tests to accomodate the new search options

* Add an index for improving delta query efficiency

* Enhances the get property fields endpoint to support deltas and hierarchical searches

* Adds new fields search endpoint

* Adds since to the property values endpoints

* Adds property value endpoint documentation

* Fix linter

* Address coderabbit comments

* Complete i18n

* Minor improvements

* Consistently apply DWIM semantics around object_type system

* Add support for DM/GMs

* Move auditRec to the top of the searchPropertyFieldsCore method

* Update since mechanics to use greater or equal

* Update API docs

* Updated tests to filter after the semantics changes

---------

Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
2026-06-22 17:27:02 +02:00

758 lines
30 KiB
YAML

"/api/v4/properties/groups/{group_name}/{object_type}/fields":
post:
tags:
- properties
summary: Create a property field
description: >
Create a new property field for a specific group and object type.
operationId: CreatePropertyField
parameters:
- name: group_name
in: path
description: The name of the property group
required: true
schema:
type: string
- name: object_type
in: path
description: The type of object this property field applies to
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
type: object
required:
- name
- type
- target_type
properties:
name:
type: string
description: The name of the property field
type:
type: string
description: The type of property field
enum: [text, select, multiselect, date, user, multiuser]
attrs:
type: object
description: Additional attributes for the property field
target_type:
type: string
description: The scope level of the property
target_id:
type: string
description: The ID of the target
permission_field:
type: string
enum: [none, sysadmin, member, admin]
description: >
Permission level for editing the field definition.
Only system admins can set this; ignored for non-admin users.
default: member
permission_values:
type: string
enum: [none, sysadmin, member, admin]
description: >
Permission level for setting values on objects.
Only system admins can set this; ignored for non-admin users.
default: member
permission_options:
type: string
enum: [none, sysadmin, member, admin]
description: >
Permission level for managing options on select/multiselect fields.
Only system admins can set this; ignored for non-admin users.
default: member
linked_field_id:
type: string
description: >
The ID of a template field to link to. The source must be a
template field in the same group, must not itself be linked, and must
not be deleted. When set, the created field inherits the source's type,
options, and security attributes; the `type` field in the request body
is ignored. Can only be set at creation time.
description: Property field object to create
required: true
responses:
"201":
description: Property field creation successful
content:
application/json:
schema:
$ref: "#/components/schemas/PropertyField"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"409":
description: >
A property field with the same name already exists at the same or a
conflicting hierarchy level.
$ref: "#/components/responses/Conflict"
get:
tags:
- properties
summary: Get property fields
description: >
Get a list of property fields for a specific group and object type. Uses cursor-based pagination.
**Scope modes (mutually exclusive):**
- **Hierarchical scope** (`channel_id` and/or `team_id`): returns fields scoped to the named resource *and every ancestor scope above it*. System-level rows are always included; team-level rows are included when `team_id` is set; channel-level rows are included only when `channel_id` is set. When `channel_id` is provided, the channel's team is resolved server-side, so any `team_id` the caller passes is ignored. For DM and GM channels there is no parent team, so the hierarchy collapses to `system → channel` and no team-scoped rows are returned. Requires the caller to have `read_channel` for the channel and/or `view_team` for the team.
- **Single-target scope** (`target_type` + `target_id`): returns fields scoped to exactly one resource. `target_type` must be `system`, `team`, or `channel`; `target_id` is required for `team` and `channel`.
Mixing the two modes (any of `channel_id`/`team_id` together with any of `target_type`/`target_id`) returns 400. Omitting both modes also returns 400, except for the system case below.
**System-object:** when `object_type` is `system`, the endpoint always resolves to `target_type=system` and ignores any `channel_id`, `team_id`, `target_type`, or `target_id` the caller may have passed. System-object fields can only live at the system scope by invariant, so any other scope is a semantic no-op rather than an error.
**Delta sync via `since`:** When `since > 0`, the endpoint returns rows whose `update_at` is **greater than or equal to** the cutoff, *including* tombstoned rows (`delete_at > 0`). The inclusive boundary lets clients safely re-use the highest `update_at` seen on a previous page as the next `since` value without missing rows that happened to share that millisecond. Rows within a delta page are ordered by `(update_at, id)` and the cursor disambiguates same-millisecond rows across pages.
**Cursor source:** the client owns the cursor. The initial delta-sync request omits `since` (or sends `since=0`). For subsequent requests the client persists the `update_at` and `id` of the last row in the response and passes them back as `cursor_update_at` and `cursor_id` alongside the same `since` value. This mirrors the cursor convention used by the shared-channel post-sync API.
**Cursor key must match the active ordering:** in delta mode (`since > 0`) the endpoint orders by `update_at` and pagination requires `cursor_update_at`; otherwise it orders by `create_at` and pagination requires `cursor_create_at`. Passing the wrong key for the active mode returns 400.
operationId: GetPropertyFields
parameters:
- name: group_name
in: path
description: The name of the property group
required: true
schema:
type: string
- name: object_type
in: path
description: The type of object to retrieve property fields for
required: true
schema:
type: string
- name: channel_id
in: query
description: >
Hierarchical scope. When set, the response includes system-level
rows, team-level rows for the channel's team, and channel-level
rows for this channel. Mutually exclusive with `target_type` and
`target_id`. Requires `read_channel` on the channel.
schema:
type: string
- name: team_id
in: query
description: >
Hierarchical scope. When set without `channel_id`, the response
includes system-level and team-level rows for this team. When
`channel_id` is also set, this value is ignored — the channel's
team is resolved server-side and used instead. Mutually exclusive
with `target_type` and `target_id`. Requires `view_team` on the
team.
schema:
type: string
- name: target_type
in: query
description: >
Single-target scope. One of `system`, `team`, or `channel`.
Required in single-target mode. Mutually exclusive with `channel_id`
and `team_id`.
schema:
type: string
enum:
- system
- team
- channel
- name: target_id
in: query
description: >
Single-target scope. Required when `target_type` is `channel` or
`team`. Mutually exclusive with `channel_id` and `team_id`.
schema:
type: string
- name: since
in: query
description: >
Unix timestamp in milliseconds. When greater than 0, returns
fields with `update_at` greater than or equal to this value,
including tombstones.
schema:
type: integer
format: int64
- name: cursor_id
in: query
description: The ID of the last property field from the previous page, for cursor-based pagination.
schema:
type: string
- name: cursor_create_at
in: query
description: >
The `create_at` timestamp of the last property field from the
previous page. Required alongside `cursor_id` when `since` is
absent. Mutually exclusive with `cursor_update_at`.
schema:
type: integer
format: int64
- name: cursor_update_at
in: query
description: >
The `update_at` timestamp of the last property field from the
previous page. Required alongside `cursor_id` when `since` is
present. Mutually exclusive with `cursor_create_at`.
schema:
type: integer
format: int64
- name: per_page
in: query
description: The number of property fields per page.
schema:
type: integer
default: 60
responses:
"200":
description: Property fields retrieval successful
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/PropertyField"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"/api/v4/properties/groups/{group_name}/fields/search":
post:
tags:
- properties
summary: Search property fields across multiple object types
description: >
Returns matching fields across every requested object type in one
response. The request body is a `PropertyFieldSearch` object whose
`object_types` field lists the object types to include.
Scope, `since`, cursor, and permission semantics are identical to the
get property fields endpoint, including the system-object collapse:
when `object_types` is exactly `["system"]`, any scope or target
params in the body are ignored and the endpoint resolves to
`target_type=system`. Any other combination without an explicit
scope returns 400.
Requesting a single value in `object_types` is equivalent to calling
the singular endpoint and is supported for client uniformity.
operationId: SearchPropertyFields
parameters:
- name: group_name
in: path
description: The name of the property group
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- object_types
properties:
object_types:
type: array
minItems: 1
description: >
One or more object types to include in the response. At
least one value is required; unknown values return 400.
items:
type: string
enum:
- post
- channel
- user
- template
- system
channel_id:
type: string
description: >
Hierarchical scope. When set, the response includes
system-level rows, team-level rows for the channel's
team, and channel-level rows for this channel across
every requested `object_types`. Mutually exclusive with
`target_type`/`target_id`. Requires `read_channel` on
the channel.
team_id:
type: string
description: >
Hierarchical scope. When set without `channel_id`, the
response includes system-level and team-level rows for
this team across every requested `object_types`. When
`channel_id` is also set, this value is ignored — the
channel's team is resolved server-side and used instead.
Mutually exclusive with `target_type`/`target_id`.
Requires `view_team` on the team.
target_type:
type: string
enum:
- system
- team
- channel
description: >
Single-target scope. Mutually exclusive with `channel_id`
and `team_id`. Required if no hierarchical scope is
given, except when `object_types` is exactly `["system"]`
— in that case any scope or target params are ignored
and the endpoint resolves to `target_type=system`.
target_id:
type: string
description: >
Single-target scope. Required when `target_type` is
`channel` or `team`. Mutually exclusive with `channel_id`
and `team_id`.
since:
type: integer
format: int64
description: >
Unix timestamp in milliseconds. When greater than 0,
returns fields with `update_at` greater than or equal
to this value, including tombstones.
cursor_id:
type: string
description: The ID of the last property field from the previous page, for cursor-based pagination.
cursor_create_at:
type: integer
format: int64
description: >
The `create_at` timestamp of the last property field
from the previous page. Required alongside `cursor_id`
when `since` is absent. Mutually exclusive with
`cursor_update_at`.
cursor_update_at:
type: integer
format: int64
description: >
The `update_at` timestamp of the last property field
from the previous page. Required alongside `cursor_id`
when `since` is present. Mutually exclusive with
`cursor_create_at`.
per_page:
type: integer
default: 60
description: The number of property fields per page.
responses:
"200":
description: Property fields retrieval successful
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/PropertyField"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"/api/v4/properties/groups/{group_name}/{object_type}/fields/{field_id}":
patch:
tags:
- properties
summary: Update a property field
description: >
Partially update a property field by providing only the fields you want to update. Omitted fields will not be updated.
The `attrs` object uses merge semantics: only the keys present in the patch are updated; omitted keys are preserved. Setting a key to `null` removes it from attrs.
**Immutable fields:** `target_type`, `target_id`, and `object_type` cannot be changed after creation and are ignored if included in the patch.
**Linked fields:** Fields with a `linked_field_id` cannot have their `type` or `attrs.options` modified (returns 400).
The `linked_field_id` can only be cleared (set to empty string `""`) to unlink the field; it cannot be changed to a different value.
For non-linked fields, `linked_field_id` cannot be set to a new value (linking is only allowed at creation time).
**Propagation:** When a template field's options are updated, the changes propagate atomically to all fields that link to it.
operationId: UpdatePropertyField
parameters:
- name: group_name
in: path
description: The name of the property group
required: true
schema:
type: string
- name: object_type
in: path
description: The type of object this property field applies to
required: true
schema:
type: string
- name: field_id
in: path
description: Property field ID
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/PropertyFieldPatch"
description: Property field patch object
required: true
responses:
"200":
description: Property field update successful
content:
application/json:
schema:
$ref: "#/components/schemas/PropertyField"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"409":
description: >
Name conflict with an existing field, or cannot change type of a
field that has active linked dependents.
$ref: "#/components/responses/Conflict"
delete:
tags:
- properties
summary: Delete a property field
description: >
Deletes a property field and all its associated values.
Returns 409 Conflict if the field has active linked dependents;
unlink or delete dependent fields first.
operationId: DeletePropertyField
parameters:
- name: group_name
in: path
description: The name of the property group
required: true
schema:
type: string
- name: object_type
in: path
description: The type of object this property field applies to
required: true
schema:
type: string
- name: field_id
in: path
description: Property field ID
required: true
schema:
type: string
responses:
"200":
description: Property field deletion successful
content:
application/json:
schema:
$ref: "#/components/schemas/StatusOK"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"409":
description: >
The field has active linked dependents. Unlink or delete dependent
fields before deleting the source.
$ref: "#/components/responses/Conflict"
"/api/v4/properties/groups/{group_name}/{object_type}/values/{target_id}":
get:
tags:
- properties
summary: Get property values for a target
description: >
Get all property values for a specific target within a group. Uses cursor-based pagination.
The `template` object type cannot have values and will return 400.
The `system` object type must use the dedicated `/api/v4/properties/groups/{group_name}/system/values` endpoint and will return 400 on this route.
**Delta sync via `since`:** When `since > 0`, the endpoint returns only rows whose `update_at` is greater than the cutoff, *including* tombstoned rows.
**Cursor key must match the active ordering:** in delta mode (`since > 0`) the endpoint orders by `update_at` and pagination requires `cursor_update_at`; otherwise it orders by `create_at` and pagination requires `cursor_create_at`.
operationId: GetPropertyValues
parameters:
- name: group_name
in: path
description: The name of the property group
required: true
schema:
type: string
- name: object_type
in: path
description: The type of object
required: true
schema:
type: string
- name: target_id
in: path
description: The ID of the target object
required: true
schema:
type: string
- name: since
in: query
description: >
Unix timestamp in milliseconds. When greater than 0, returns
values with `update_at` greater than or equal to this value,
including tombstones.
schema:
type: integer
format: int64
- name: cursor_id
in: query
description: The ID of the last property value from the previous page, for cursor-based pagination.
schema:
type: string
- name: cursor_create_at
in: query
description: >
The `create_at` timestamp of the last property value from the
previous page. Required alongside `cursor_id` when `since` is
absent. Mutually exclusive with `cursor_update_at`.
schema:
type: integer
format: int64
- name: cursor_update_at
in: query
description: >
The `update_at` timestamp of the last property value from the
previous page. Required alongside `cursor_id` when `since` is
present. Mutually exclusive with `cursor_create_at`.
schema:
type: integer
format: int64
- name: per_page
in: query
description: The number of property values per page.
schema:
type: integer
default: 60
responses:
"200":
description: Property values retrieval successful
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/PropertyValue"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
patch:
tags:
- properties
summary: Update property values for a target
description: >
Update one or more property values for a specific target within a group. Uses upsert semantics: creates the value if it doesn't exist, updates it if it does. All field IDs must belong to the specified group.
The `template` object type cannot have values and will return 400.
The `system` object type must use the dedicated `/api/v4/properties/groups/{group_name}/system/values` endpoint and will return 400 on this route.
operationId: UpdatePropertyValues
parameters:
- name: group_name
in: path
description: The name of the property group
required: true
schema:
type: string
- name: object_type
in: path
description: The type of object
required: true
schema:
type: string
- name: target_id
in: path
description: The ID of the target object
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
type: array
items:
type: object
required:
- field_id
- value
properties:
field_id:
type: string
description: The ID of the property field
value:
description: The value to set for this property. Can be any JSON type.
description: Array of property values to update
required: true
responses:
"200":
description: Property values update successful
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/PropertyValue"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"/api/v4/properties/groups/{group_name}/system/values":
get:
tags:
- properties
summary: Get property values for the system
description: >
Get all property values attached to the Mattermost instance itself
within a group. System-scoped values are readable by any authenticated
user. This endpoint is the dedicated route for `system` object type;
the `{object_type}/values/{target_id}` route returns 400 for `system`.
**Delta sync via `since`:** When `since > 0`, the endpoint returns only rows whose `update_at` is greater than the cutoff, *including* tombstones.
**Cursor key must match the active ordering:** in delta mode (`since > 0`) the endpoint orders by `update_at` and pagination requires `cursor_update_at`; otherwise it orders by `create_at` and pagination requires `cursor_create_at`.
operationId: GetSystemPropertyValues
parameters:
- name: group_name
in: path
description: The name of the property group
required: true
schema:
type: string
- name: since
in: query
description: >
Unix timestamp in milliseconds. When greater than 0, returns
values with `update_at` greater than or equal to this value,
including tombstones.
schema:
type: integer
format: int64
- name: cursor_id
in: query
description: The ID of the last property value from the previous page, for cursor-based pagination.
schema:
type: string
- name: cursor_create_at
in: query
description: >
The `create_at` timestamp of the last property value from the
previous page. Required alongside `cursor_id` when `since` is
absent. Mutually exclusive with `cursor_update_at`.
schema:
type: integer
format: int64
- name: cursor_update_at
in: query
description: >
The `update_at` timestamp of the last property value from the
previous page. Required alongside `cursor_id` when `since` is
present. Mutually exclusive with `cursor_create_at`.
schema:
type: integer
format: int64
- name: per_page
in: query
description: The number of property values per page.
schema:
type: integer
default: 60
responses:
"200":
description: System property values retrieval successful
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/PropertyValue"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
patch:
tags:
- properties
summary: Update property values for the system
description: >
Update one or more property values attached to the Mattermost instance
itself. Uses upsert semantics: creates the value if it doesn't exist,
updates it if it does. Requires system administrator access. All field
IDs must reference `system` object-type fields in the specified group;
template field IDs are rejected with 400.
operationId: UpdateSystemPropertyValues
parameters:
- name: group_name
in: path
description: The name of the property group
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
type: array
items:
type: object
required:
- field_id
- value
properties:
field_id:
type: string
description: The ID of the property field
value:
description: The value to set for this property. Can be any JSON type.
description: Array of property values to update
required: true
responses:
"200":
description: System property values update successful
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/PropertyValue"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"