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_workspacebefore 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:
- Call
create_typewithname = "Article",description = "A long-form written piece", no icon. - 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:
- Call
create_typewithname = "World Event". - Observe the
slugfield in the returnedTypeDefinition.
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:
- Call
create_typewithname = "Location"andcreate_typewithname = "Character". - Call
list_types. - 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:
- Call
create_typewithname = "Region". Capture the returnedid. - Call
get_typewith thatid.
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:
- Call
create_typewithname = "Draft". Capture the returnedid. - Call
update_typewithid,name = "Finished Article",description = "Published piece",icon = "📰". - Call
get_typewith the sameid.
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:
- Call
list_typesto find the system type withslug = "page". Capture itsid. - Call
update_typewithid,icon = "📄"(nonamefield). - Call
update_typewith the sameid,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:
- Call
create_typewithname = "Temporary". Capture the returnedid. - Call
delete_typewith thatid. - 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:
- Call
list_typesto locate the type withslug = "folder". Capture itsid. - Attempt to call
delete_typewith thatid.
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:
- Attempt to call
create_typewithname = "".
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:
- Call
create_typewithname = "Character". Capturetype_id. - Call
create_pagewithtitle = "Aria". Capturepage_id. - Call
assign_type_to_pagewithpage_idandtype_id. - Call
get_page_typeswithpage_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:
- Call
create_typewithname = "Location". Capturetype_id. - Call
create_pagewithtitle = "Castle". Capturepage_id. - Call
assign_type_to_pagewithpage_idandtype_id. (First assignment — succeeds.) - Call
assign_type_to_pageagain with the samepage_idandtype_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:
- Call
create_typewithname = "NPC". Capturetype_id. - Call
create_pagewithtitle = "Guard". Capturepage_id. - Call
assign_type_to_pagewithpage_idandtype_id. - Call
remove_type_from_pagewithpage_idandtype_id. - Call
get_page_typeswithpage_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:
- Call
create_propertywithname = "Birth Year",value_type = "number". - 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:
- Call
create_typewithname = "Faction". Capturetype_id. - Call
create_propertywithname = "Allegiance",value_type = "text". Captureproperty_id. - Call
add_property_to_typewithtype_idandproperty_id. - Call
get_typewithtype_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:
- Call
create_typewithname = "Artifact". Capturetype_id. - Call
create_propertywithname = "Age",value_type = "number". Captureproperty_id. - Call
add_property_to_typewithtype_idandproperty_id. - Call
remove_property_from_typewithtype_idandproperty_id. - Call
get_typewithtype_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:
- Call
create_pagewithtitle = "Elara". Capturepage_id. - Call
create_propertywithname = "Age",value_type = "number". - Call
set_property_valuewithpage_id,property_slug = "age",value = 34(JSON number). - Call
get_page_propertieswithpage_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:
- Call
create_pagewithtitle = "Ancient Tome". Capturepage_id. - Call
set_property_valuewithproperty_slug = "era",value = "Third Age". - Call
set_property_valueagain withproperty_slug = "rarity",value = "Legendary". - Call
get_page_propertieswithpage_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:
- Call
create_typewithname = "Creature". Capturetype_id. - Call
create_propertywithname = "CR",value_type = "number". Captureproperty_id. - Call
add_property_to_typewithtype_idandproperty_id. - Call
create_pagewithtitle = "Owlbear". Capturepage_id. - Call
assign_type_to_pagewithpage_idandtype_id. - Call
set_property_valuewithpage_id,property_slug = "cr",value = 3. - Call
get_page_propertieswithpage_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:
- Call
create_pagewithtitle = "Ephemeral". Capturepage_id. - Call
set_property_valuewithproperty_slug = "temp-note",value = "delete me". - Call
set_property_valuewith the sameproperty_slug,value = null. - Call
get_page_propertieswithpage_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:
- Call
create_pagewithtitle = "Validated Page". Capturepage_id. - Call
create_propertywithname = "Count",value_type = "number". - Call
set_property_valuewithproperty_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:
- Call
create_pagewithtitle = "Tag Test Page". Capturepage_id. - Call
create_propertywithname = "Themes",value_type = "multi_select". - Call
set_property_valuewithproperty_slug = "themes",value = ["Action", "Drama"]. - Call
set_property_valuewithproperty_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:
- Call
create_propertywithname = "Status",value_type = "select", andconfig = { "options": [{ "label": "Draft", "color": null }, { "label": "Published", "color": "#22c55e" }] }. - Call
get_property(or inspect the returned object fromcreate_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:
- Call
create_typewithname = "Persistent Type". - Call
close_workspace(or equivalent). - Call
open_workspaceon the same workspace path. - 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:
- Call
create_typewithname = "Disposable Type". Capturetype_id. - Call
create_pagewithtitle = "Test Page". Capturepage_id. - Call
assign_type_to_pagewithpage_idandtype_id. - Call
delete_typewithtype_id. - Call
get_page_typeswithpage_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:
- Call
create_pagewithtitle = "Orphan Page". Capturepage_id. - Construct a random UUID that does not correspond to any type.
- Call
assign_type_to_pagewithpage_idand the non-existenttype_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
| Key | Value | Notes |
|---|---|---|
| system_type_page_id | 00000000-0000-0000-0000-000000000001 | Deterministic UUID for built-in “Page” type |
| system_type_folder_id | 00000000-0000-0000-0000-000000000002 | Deterministic UUID for built-in “Folder” type |
| system_prop_summary_id | 00000000-0000-0000-0000-000000000011 | Deterministic UUID for built-in “summary” property |
| user_type_name | Character | Canonical user-defined type name used across scenarios |
| user_type_slug | character | Slug derived from “Character” |
| prop_value_types | text, number, boolean, date, select, multi_select, relation | All supported value_type variants |
| multi_select_config | {"options":[{"label":"A","color":null}]} | Minimal valid config for multi_select property |
| max_name_length | 100 | Both type and property names reject strings over 100 characters |
Notes
create_typeauto-generates the slug viadomain::slugify(&name). Slugs are lowercase, digits, and hyphens only — underscores and spaces are converted or stripped. Do not pass an explicitslugargument; it is derived server-side.update_typeuses a tri-state pattern fordefault_layout_id: omitting the field leaves it unchanged, passingnullclears 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 aresummary(0x11),cover_image(0x12),tags(0x13), andaliases(0x14). value_typeis immutable after a property definition is created. Attempting to change it viaupdate_propertyreturns aValueTypeImmutableerror.get_page_propertiesmerges 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 withis_from_type = falseandproperty_id = "00000000-0000-0000-0000-000000000000"(nil UUID).- The HTTP bridge routes for
list_typesandget_page_typesinapps/http-bridge/src/routes/types.rsare 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_typecascades 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?
Thanks for your feedback!