SAC - Edits concept
| tags | initiative streetartcities |
|---|---|
| date | 2026-05-31 |
| status | in-progress |
Edits are a new way to make or suggest edits in Street Art Cities.
Any change to a marker or artist, whether automatically accepted (e.g. a hunter uploading in their own city), or queued for review (e.g. a visitor suggesting a tag to be added to an existing marker), is first created as an edit.
This provides a coherent API for such changes, and will speed up implementing new features in the front-end a lot.
What changes can apply to
- Existing markers
- E.g. hunter edits title of their artwork
- E.g. visitor suggest change to artist name
- E.g. visitor suggest additional tag
- E.g. hunter marks artwork as duplicate of another artwork
- New markers
- E.g. hunter submits new artwork in their city
- E.g. visitor suggests a new artwork from the app
- E.g. hunter suggests new artwork in other city
- Existing artists
- E.g. visitor suggests additional tag
- E.g. visitor suggests name change
- E.g. artist edits bio
Possibly can be applied to more entities later, the system should be built in such a way that it is extensible (e.g. also allowing metadata changes to cities).
Edit pipeline
The rough process for every edit is as follows:
- User submits edit (API call, saved into DB, if structure is valid)
- Permissions check is run to see if we can auto-accept this edit
- If so, immediately apply the edit to the entity, and mark the edit as
accepted, withreviewedBy=systemand areviewNotestating the reason it was accepted (e.g.User is hunter of site X,User is admin,User created original entity, etc.) - If not, the edit sits waiting for review. Once accepted or rejected, the status of the edit is updated, with the
reviewedBy,reviewedAtand optionalreviewNotesaved, as well as a new entry in thehistoryarray. The edit is then applied to the original entity.
Permissions checks
The permissions checks will be an extensible set of checks. The initial version will look like this:
- If user is in group
admins, auto accept edit - If user is the original
createdByof the entity, auto accept - If the current version of the entity has a
sitefield, and if user is in group{siteIdOfEntity}:hunter, auto accept - If the current version of the entity has a
sitefield, and if user is in group{siteIdOfEntity}:insights, auto accept - If the current version of the entity has a
countryfield, and if the user is in group{countryOfEntity}:country-manager, auto accept
Otherwise, the edit ends up in the review queue. In the future, we might add additional checks here, e.g. to check for the reputation of the user. Higher-reputation users might be able to take certain actions, even if they don't have explicit permissions. This is not for v1 though, but should be built in a way that is easily extendable.
Assigned reviewers
If the item isn't auto-accepted, we should collect a set of eligible reviewers. This should also be easily extendable, but the rules for v1 are:
- Original creator (
createdBy) of existing entity, if any - If the current version of the entity (or proposed version if entity doesn't exist) has a
sitefield, all users in group{site}:hunter - If the current version of the entity (or proposed version if entity doesn't exist) has a
countryfield, all users in group{country}:country-manager - All users in group
admins
These groups are added as assignedReviewers (either user ID in case of original creator, group names in other cases), and notifications are sent to all matching users with a link to a page where they can review and accept/reject the edit.
Reverting changes
We want to have a system that allows reverting changes. For v1, to keep it simple, we're going to make a distinction in the type of change:
Clean changes
A change is considered "clean" if the current version of the entity matches all of the changes that were applied.
Example: if we have a entity that looks like this:
{
"id": "123",
"title": "Hello universe",
"description": "...",
"attributes": {
"artwork_style": ["Surrealism"],
"artist_nationality": ["Greek", "Dutch"]
}
}
And an applied change with these actions:
{
"title": "Hello universe",
"attrributes.artist_nationality": {
"$add": ["Greek"]
}
}
That is considered a clean change - all of the actions in the change are still in place, nothing has overwritten it.
Dirty changes
A dirty change would be something like this, if it had been applied in the past to the entity shown above, and afterwards other changes have been applied, or the original change has been partially reverted:
{
"title": "Hello world",
"attrributes.artist_nationality": {
"$add": ["Bulgarian"]
}
}
Choice for v1
For v1, we're only allow reverting clean changes. When trying to revert a dirty change, error with an informative message about why this change can't be reverted automatically.
Entity merges can't be reverted at all, for now.
Anatomy of a change
When a change specifies an entityId, it edits an existing entity. Alternatively, it specifies a entityType, denoting a new entity of that type is to be created when accepted.
| Field | Type | Example | Required | Notes |
|---|---|---|---|---|
| entityId | string | 9ff36a03-67ee-4b11-9d80-f5a1421f7c31 |
conditionally | Required when no entityType is specified |
| entityType | string | marker |
conditionally | Required when no entityId is specified |
| actions | Actions |
(see below) | yes | |
| createdBy | string | |||
| createdAt | ISO date string | |||
| reviewedBy | string | system |
||
| reviewedAt | ISO date string | |||
| revertedBy | string | |||
| revertedAt | ISO date string | |||
| status | enum | submitted | accepted | rejected | reverted |
yes | Defaults to submitted |
| editComment | string | "Added name of additional artist" | ||
| reviewComment | string | "Looks like spam, rejected." | ||
| assignedReviewers | string[] |
|||
| snapshotOld | Object | Snapshot of what the updated fields looked like before the update. Populated when the edit is accepted. | ||
| snapshotNew | Object | Snapshot of what the updated fields looked like after the update. Populated when the edit is accepted. | ||
| history | HistoryState[] |
Array of state changes, e.g. [{status:"submitted", createdAt: "...", createdBy: "..."}, ...]. Used to be able to see the full history of who submitted->reviewed->reverted |
||
| attributes |
Change actions
The type Actions can look like either of these examples:
Simple field edit/create:
{
"attributes.access_note": "Only accessible whilst shop is open."
}
Array manipulations (for tags, images, artists):
{
"attributes.artist_nationality": {
"$add": ["Moroccan"],
"$remove": ["Marrakechi"]
}
}
Delete fields:
{
"title": {
"$unset": true
}
}
Merge entity into other entity (needs to be same type!)
{
"$mergeInto": "99dea3f7-3d48-431a-906d-ad5f61c7863f"
}
Actions nested keys
Note that this means we also allow nested keys, but changes are always applied by only looking at the first level of the tree.
❌ INVALID EXAMPLE:
{
"attributes": {
"artwork_style": {
"$add": ["Surrealism"]
}
}
}
Doing this will result in the attributes of the entity being completely overwritten with the literal value {"artwork_style": {"$add": "surrealism"}}, which is never what you want, since we should venture to make changes as specific as possible, to allow stacking them (two users might suggest changes to different parts of the entity at the same time).
Grouping actions
Multiple actions can be grouped in a single change. For example, it is totally acceptable to do the first four examples from above in a single change:
{
"attributes.access_note": "Only accessible whilst shop is open.",
"attributes.artist_nationality": {
"$add": ["Moroccan"],
"$remove": ["Marrakechi"]
},
"title": {
"$unset": true
},
"$mergeInto": "99dea3f7-3d48-431a-906d-ad5f61c7863f"
}
Which has the result of setting the access note, removing Marrakechi and adding Moroccan to the artist nationality attribute, removing the title, and merging into the specified entity.
Actions are applied in the order they are returned by the JSON parser and Object.entities(actions). This does mean, however, that two changes to the same field (repeat keys) might not lead to the intended effect, and should be avoided.
Special sub-keys
If the value of any item in the Actions record (right-hand side of any expression in the JSON object) is an object and contains any of the following keys: $add, $remove, $unset, that object is treated as a Operation (more possible operations to be added potentially in the future).
Any other value, including objects of any other shape, are treated as literal values for that field.