Skip to content
Documentation GitHub
Workspace

Type Definitions & Property Definitions

Type Definitions & Property Definitions

Covers the full lifecycle of custom page types and property definitions: creating, updating, and deleting type definitions; assigning and removing types on pages; attaching property definitions to types; setting and validating property values; and verifying that system types (Page, Folder) and system properties are protected. This spec is P1 because the type system is the foundation of structured data in Inklings — bugs in type assignment or property value persistence corrupt every page that relies on that type.

The type definition system has three layers: workspace-scoped TypeDefinition records (name, slug, icon, property_ids), workspace-scoped PropertyDefinition records (name, slug, value_type, config), and per-page TypeAssignment records (page_id, type_id, scope). Commands tested here are create_type, update_type, delete_type, list_types, get_type, assign_type_to_page, remove_type_from_page, get_page_types, create_property, update_property, delete_property, add_property_to_type, remove_property_from_type, set_property_value, and get_page_properties.

Preconditions

  • HTTP bridge running on port 9990
  • A workspace initialized via initialize_workspace before each scenario
  • Bridge shim injected via playwright.config.ts

Scenarios

Seed: seed.spec.ts

1. Create a type definition

Creating a user-defined type with a name generates a slug automatically and returns the persisted definition.

Steps:

  1. Call create_type with name = "Article", description = "A long-form written piece", no icon.
  2. Capture the returned TypeDefinition.

Expected: The returned object has name = "Article", slug = "article", is_system = false, a non-nil UUID id, non-empty created_at and updated_at timestamps, and an empty property_ids list. The description field equals "A long-form written piece".

2. Type slug is derived from the name

The system auto-generates a URL-safe slug from the type name by lowercasing and replacing spaces with hyphens.

Steps:

  1. Call create_type with name = "World Event".
  2. Observe the slug field in the returned TypeDefinition.

Expected: slug equals "world-event". No uppercase characters or spaces appear in the slug.

3. List all type definitions

list_types returns all user-defined types plus the two built-in system types.

Steps:

  1. Call create_type with name = "Location" and create_type with name = "Character".
  2. Call list_types.
  3. Inspect the returned array.

Expected: The array contains at least 4 entries: the 2 user-defined types (“Location”, “Character”) and the 2 system types (“Page”, “Folder”) with is_system = true. All entries have valid UUIDs and timestamps. The list is ordered by sort_order.

4. Get a type definition by ID

get_type retrieves a specific type definition by its UUID.

Steps:

  1. Call create_type with name = "Region". Capture the returned id.
  2. Call get_type with that id.

Expected: The returned TypeDefinition matches the one returned by step 1: same id, name = "Region", slug = "region", is_system = false.

5. Update a type definition name, description, and icon

All mutable fields of a user-defined type can be patched independently.

Steps:

  1. Call create_type with name = "Draft". Capture the returned id.
  2. Call update_type with id, name = "Finished Article", description = "Published piece", icon = "📰".
  3. Call get_type with the same id.

Expected: get_type returns name = "Finished Article", description = "Published piece", icon = "📰". The slug is updated to reflect the new name (e.g., "finished-article"). The updated_at timestamp is later than created_at.

6. Update only the icon on a system type — name is protected

System types allow icon/color updates but reject name changes.

Steps:

  1. Call list_types to find the system type with slug = "page". Capture its id.
  2. Call update_type with id, icon = "📄" (no name field).
  3. Call update_type with the same id, name = "Renamed Page".

Expected: Step 2 succeeds and the returned type shows icon = "📄". Step 3 fails with a validation error containing the phrase “system type”. The system type’s name remains “Page” throughout.

7. Delete a user-defined type

A non-system type can be permanently deleted.

Steps:

  1. Call create_type with name = "Temporary". Capture the returned id.
  2. Call delete_type with that id.
  3. Call list_types.

Expected: delete_type returns no error. The subsequent list_types response does not contain a type with slug = "temporary". get_type with the deleted id returns a NotFound error.

8. Deleting a system type is rejected

The built-in “Page” and “Folder” types cannot be deleted regardless of any attempt.

Steps:

  1. Call list_types to locate the type with slug = "folder". Capture its id.
  2. Attempt to call delete_type with that id.

Expected: The call returns a validation or system-type error. list_types still includes the “Folder” system type. No data is corrupted.

9. Create a type name validation — empty name rejected

An empty string is not a valid type name.

Steps:

  1. Attempt to call create_type with name = "".

Expected: A validation error is returned containing the word “empty”. No type is created in the workspace.

10. Assign a type to a page (Manual scope)

A user-defined type can be manually assigned to any page.

Steps:

  1. Call create_type with name = "Character". Capture type_id.
  2. Call create_page with title = "Aria". Capture page_id.
  3. Call assign_type_to_page with page_id and type_id.
  4. Call get_page_types with page_id.

Expected: assign_type_to_page returns a TypeAssignment with scope = "manual", the correct page_id, and the correct type_id. get_page_types returns an array containing that assignment.

11. Duplicate type assignment on the same page is rejected

Assigning the same type to a page twice must be caught before persisting.

Steps:

  1. Call create_type with name = "Location". Capture type_id.
  2. Call create_page with title = "Castle". Capture page_id.
  3. Call assign_type_to_page with page_id and type_id. (First assignment — succeeds.)
  4. Call assign_type_to_page again with the same page_id and type_id. (Second attempt.)

Expected: The second call returns an AlreadyExists validation error. get_page_types still shows exactly one assignment for that type, not two.

12. Remove a type assignment from a page

A manually assigned type can be removed from a page.

Steps:

  1. Call create_type with name = "NPC". Capture type_id.
  2. Call create_page with title = "Guard". Capture page_id.
  3. Call assign_type_to_page with page_id and type_id.
  4. Call remove_type_from_page with page_id and type_id.
  5. Call get_page_types with page_id.

Expected: remove_type_from_page succeeds (returns no error). get_page_types returns an empty array or an array that no longer contains the “NPC” type assignment.

13. Create a property definition

A workspace-scoped property definition can be created with a name and value type.

Steps:

  1. Call create_property with name = "Birth Year", value_type = "number".
  2. Capture the returned PropertyDefinition.

Expected: The returned object has name = "Birth Year", slug = "birth-year", value_type = "number", is_system = false, and a non-nil id.

14. Add a property to a type and verify it appears in property_ids

Linking a property definition to a type causes the property to appear in the type’s property_ids list.

Steps:

  1. Call create_type with name = "Faction". Capture type_id.
  2. Call create_property with name = "Allegiance", value_type = "text". Capture property_id.
  3. Call add_property_to_type with type_id and property_id.
  4. Call get_type with type_id.

Expected: The returned TypeDefinition.property_ids array contains the property_id value. The association is reflected immediately.

15. Remove a property from a type

A linked property can be detached from a type without deleting the property definition itself.

Steps:

  1. Call create_type with name = "Artifact". Capture type_id.
  2. Call create_property with name = "Age", value_type = "number". Capture property_id.
  3. Call add_property_to_type with type_id and property_id.
  4. Call remove_property_from_type with type_id and property_id.
  5. Call get_type with type_id.

Expected: remove_property_from_type succeeds. The property_ids array in the returned type definition no longer contains property_id. The property definition itself still exists (it was not deleted, only unlinked).

16. Set a property value on a page

set_property_value stores a typed frontmatter value for a given page.

Steps:

  1. Call create_page with title = "Elara". Capture page_id.
  2. Call create_property with name = "Age", value_type = "number".
  3. Call set_property_value with page_id, property_slug = "age", value = 34 (JSON number).
  4. Call get_page_properties with page_id.

Expected: set_property_value returns null (no error). get_page_properties includes an entry with slug = "age", value = 34, value_type = "number", and is_from_type = false (since no type assignment links this property to the page yet).

17. Property value persists across page re-opens

A property value written to frontmatter is retrieved correctly in subsequent calls.

Steps:

  1. Call create_page with title = "Ancient Tome". Capture page_id.
  2. Call set_property_value with property_slug = "era", value = "Third Age".
  3. Call set_property_value again with property_slug = "rarity", value = "Legendary".
  4. Call get_page_properties with page_id.

Expected: get_page_properties returns two freeform entries: { slug: "era", value: "Third Age" } and { slug: "rarity", value: "Legendary" }. Both is_from_type flags are false (freeform).

18. Typed property appears in properties panel when type is assigned

When a type that owns a property is assigned to a page, get_page_properties returns that property with is_from_type = true.

Steps:

  1. Call create_type with name = "Creature". Capture type_id.
  2. Call create_property with name = "CR", value_type = "number". Capture property_id.
  3. Call add_property_to_type with type_id and property_id.
  4. Call create_page with title = "Owlbear". Capture page_id.
  5. Call assign_type_to_page with page_id and type_id.
  6. Call set_property_value with page_id, property_slug = "cr", value = 3.
  7. Call get_page_properties with page_id.

Expected: get_page_properties returns an entry for slug = "cr" with is_from_type = true, value = 3, and value_type = "number".

19. Clear a property value by passing null

Passing value = null to set_property_value removes the frontmatter key entirely.

Steps:

  1. Call create_page with title = "Ephemeral". Capture page_id.
  2. Call set_property_value with property_slug = "temp-note", value = "delete me".
  3. Call set_property_value with the same property_slug, value = null.
  4. Call get_page_properties with page_id.

Expected: After step 3, get_page_properties does not include an entry with slug = "temp-note". The frontmatter key was removed. No error occurs.

20. Property value type mismatch is rejected

set_property_value validates the JSON value against the registered property’s value_type.

Steps:

  1. Call create_page with title = "Validated Page". Capture page_id.
  2. Call create_property with name = "Count", value_type = "number".
  3. Call set_property_value with property_slug = "count", value = "not-a-number" (JSON string).

Expected: The call returns a validation error mentioning the slug "count" and the mismatch. The page’s frontmatter is not modified.

21. MultiSelect property accepts an array of strings

value_type = "multi_select" requires an array of strings; a scalar or array of non-strings is rejected.

Steps:

  1. Call create_page with title = "Tag Test Page". Capture page_id.
  2. Call create_property with name = "Themes", value_type = "multi_select".
  3. Call set_property_value with property_slug = "themes", value = ["Action", "Drama"].
  4. Call set_property_value with property_slug = "themes", value = [1, 2, 3].

Expected: Step 3 succeeds. Step 4 returns a validation error. The value stored after step 3 is the array ["Action", "Drama"].

22. Select option list is stored in property config

A select property definition can carry a list of predefined options with optional colors.

Steps:

  1. Call create_property with name = "Status", value_type = "select", and config = { "options": [{ "label": "Draft", "color": null }, { "label": "Published", "color": "#22c55e" }] }.
  2. Call get_property (or inspect the returned object from create_property) to read the config.

Expected: The returned PropertyDefinition.config.options contains exactly 2 entries: { label: "Draft", color: null } and { label: "Published", color: "#22c55e" }. The value_type is "select".

23. Type definitions persist across workspace re-initialization

A type created in one session is available in subsequent sessions.

Steps:

  1. Call create_type with name = "Persistent Type".
  2. Call close_workspace (or equivalent).
  3. Call open_workspace on the same workspace path.
  4. Call list_types.

Expected: The type “Persistent Type” appears in the list with its original id, slug = "persistent-type", and is_system = false. No data is lost across sessions.

24. Deleting a type cascades to remove page assignments

When a type is deleted, existing page-type assignments for that type are removed by database FK cascade.

Steps:

  1. Call create_type with name = "Disposable Type". Capture type_id.
  2. Call create_page with title = "Test Page". Capture page_id.
  3. Call assign_type_to_page with page_id and type_id.
  4. Call delete_type with type_id.
  5. Call get_page_types with page_id.

Expected: delete_type succeeds. get_page_types returns an empty array or an array that does not contain the deleted type_id. The page itself is unaffected and still accessible.

25. Assigning a non-existent type to a page is rejected

The assign_type_to_page use case verifies that the type exists before creating an assignment.

Steps:

  1. Call create_page with title = "Orphan Page". Capture page_id.
  2. Construct a random UUID that does not correspond to any type.
  3. Call assign_type_to_page with page_id and the non-existent type_id.

Expected: The call returns a NotFound error. get_page_types for the page returns an empty array — no phantom assignment was created.

Test Data

KeyValueNotes
system_type_page_id00000000-0000-0000-0000-000000000001Deterministic UUID for built-in “Page” type
system_type_folder_id00000000-0000-0000-0000-000000000002Deterministic UUID for built-in “Folder” type
system_prop_summary_id00000000-0000-0000-0000-000000000011Deterministic UUID for built-in “summary” property
user_type_nameCharacterCanonical user-defined type name used across scenarios
user_type_slugcharacterSlug derived from “Character”
prop_value_typestext, number, boolean, date, select, multi_select, relationAll supported value_type variants
multi_select_config{"options":[{"label":"A","color":null}]}Minimal valid config for multi_select property
max_name_length100Both type and property names reject strings over 100 characters

Notes

  • create_type auto-generates the slug via domain::slugify(&name). Slugs are lowercase, digits, and hyphens only — underscores and spaces are converted or stripped. Do not pass an explicit slug argument; it is derived server-side.
  • update_type uses a tri-state pattern for default_layout_id: omitting the field leaves it unchanged, passing null clears it, passing a UUID string sets it.
  • System types (is_system = true) have fixed slugs "page" and "folder" with deterministic UUIDs. Their names cannot be changed. Their icons and colors can be patched.
  • System properties (is_system = true) cannot be deleted. The built-in ones are summary (0x11), cover_image (0x12), tags (0x13), and aliases (0x14).
  • value_type is immutable after a property definition is created. Attempting to change it via update_property returns a ValueTypeImmutable error.
  • get_page_properties merges typed properties (from type assignments) and freeform frontmatter keys. A property linked to two types only appears once (deduplication by property ID). Freeform keys not matching any property definition are returned with is_from_type = false and property_id = "00000000-0000-0000-0000-000000000000" (nil UUID).
  • The HTTP bridge routes for list_types and get_page_types in apps/http-bridge/src/routes/types.rs are stubs that return empty arrays. Type and property scenarios must be executed via the Tauri desktop app or a fully-wired bridge. Confirm with the bridge maintainer before running these scenarios against the bridge.
  • delete_type cascades via database FK constraints: page assignments, type-property references, and container rules referencing the deleted type are removed automatically. No explicit cascade call is needed from the test.

Was this page helpful?