SAC - Edits concept

tagsinitiative streetartcities
date2026-05-31
statusin-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.

Inline image

What changes can apply to

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:

  1. User submits edit (API call, saved into DB, if structure is valid)
  2. Permissions check is run to see if we can auto-accept this edit
  3. If so, immediately apply the edit to the entity, and mark the edit as accepted, with reviewedBy=system and a reviewNote stating the reason it was accepted (e.g. User is hunter of site X, User is admin, User created original entity, etc.)
  4. If not, the edit sits waiting for review. Once accepted or rejected, the status of the edit is updated, with the reviewedBy, reviewedAt and optional reviewNote saved, as well as a new entry in the history array. 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:

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:

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.