-
Notifications
You must be signed in to change notification settings - Fork 2
72 Creating Document
This tutorial covers the specifics of creating an entity that extends the document class. Documents are the primary business objects in the Platform -- they have area-based access control, priority, rich state machines, and full audit trails.
| Feature | Document | Reference |
|---|---|---|
| Parent table | db.document |
db.reference |
| Parent class | GetClass('document') |
GetClass('reference') |
| Access control | Area-based (multi-tenant) | Scope-based |
| Has area | Yes (d.area) |
No |
| Has priority | Yes (d.priority) |
No |
| Localization |
db.document_text (label, description per locale) |
db.reference_text (name, description per locale) |
| Seed data | Typically created at runtime | Often seeded in FillDataBase() or Init*()
|
| Typical use | Business transactions, records | Lookup tables, catalogs |
A Document entity's table has a FK to db.document(id) with ON DELETE CASCADE:
CREATE TABLE db.sensor (
id uuid PRIMARY KEY,
document uuid NOT NULL REFERENCES db.document(id) ON DELETE CASCADE,
code text NOT NULL,
value numeric,
unit text,
metadata jsonb
);The document column links to the parent. The BEFORE INSERT trigger copies the document ID to the entity ID:
CREATE OR REPLACE FUNCTION db.ft_sensor_insert()
RETURNS trigger AS $$
BEGIN
IF NEW.id IS NULL THEN
SELECT NEW.document INTO NEW.id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = kernel, pg_temp;
CREATE TRIGGER t_sensor_insert
BEFORE INSERT ON db.sensor
FOR EACH ROW
EXECUTE PROCEDURE db.ft_sensor_insert();This ensures db.sensor.id = db.document.id = db.object.id -- all three share the same UUID. This is the core of the entity inheritance model.
For entities where you want to enforce write-access at the trigger level:
CREATE OR REPLACE FUNCTION db.ft_sensor_update()
RETURNS trigger AS $$
BEGIN
IF NOT CheckObjectAccess(NEW.document, B'010') THEN
PERFORM AccessDenied();
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = kernel, pg_temp;
CREATE TRIGGER t_sensor_update
BEFORE UPDATE ON db.sensor
FOR EACH ROW
EXECUTE PROCEDURE db.ft_sensor_update();The station entity extends device which extends document. Its table:
CREATE TABLE db.station (
id uuid PRIMARY KEY,
device uuid NOT NULL REFERENCES db.device(id) ON DELETE CASCADE,
navigation uuid NOT NULL REFERENCES db.navigation(id) ON DELETE RESTRICT,
network uuid NOT NULL REFERENCES db.network(id) ON DELETE RESTRICT,
country uuid NOT NULL REFERENCES db.country(id) ON DELETE RESTRICT,
region uuid NOT NULL REFERENCES db.region(id) ON DELETE RESTRICT,
format uuid NOT NULL REFERENCES db.format(id) ON DELETE RESTRICT,
mountpoint text NOT NULL,
-- ... more columns ...
);Note that station references device (not document directly) because it is a sub-class of device. The FK chain is: station.device -> device.document -> document.id -> object.id.
When creating a Document entity, you first call CreateDocument() which creates the db.object and db.document rows:
uDocument := CreateDocument(pParent, pType, pLabel, pDescription);Parameters:
-
pParent-- parent object UUID (can be NULL for top-level documents) -
pType-- the type UUID (determines the class) -
pLabel-- human-readable label (stored indb.object_text) -
pDescription-- description (stored indb.document_text)
CreateDocument internally:
- Calls
CreateObject()which inserts intodb.object - Inserts into
db.documentwith the current area and priority - Returns the object UUID
Then your Create function inserts the specialized row:
INSERT INTO db.sensor (id, document, code, value, unit, metadata)
VALUES (uDocument, uDocument, pCode, pValue, pUnit, pMetadata);And finally triggers the workflow:
uMethod := GetMethod(uClass, GetAction('create'));
PERFORM ExecuteMethod(uDocument, uMethod);Here is the CreateStation function pattern simplified for a generic document entity:
CREATE OR REPLACE FUNCTION CreateSensor (
pParent uuid,
pType uuid,
pCode text,
pLabel text default null,
pDescription text default null,
pValue numeric default null,
pUnit text default null,
pMetadata jsonb default null
) RETURNS uuid
AS $$
DECLARE
uDocument uuid;
uClass uuid;
uMethod uuid;
BEGIN
-- 1. Validate the type
SELECT class INTO uClass FROM db.type WHERE id = pType;
IF GetEntityCode(uClass) <> 'sensor' THEN
PERFORM IncorrectClassType();
END IF;
-- 2. Validate any FK references
-- (validate that referenced objects exist)
-- 3. Create parent document
uDocument := CreateDocument(pParent, pType, pLabel, pDescription);
-- 4. Insert specialized row
INSERT INTO db.sensor (id, document, code, value, unit, metadata)
VALUES (uDocument, uDocument, pCode, pValue, pUnit, pMetadata);
-- 5. Execute the 'create' method (fires events)
uMethod := GetMethod(uClass, GetAction('create'));
PERFORM ExecuteMethod(uDocument, uMethod);
RETURN uDocument;
END;
$$ LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = kernel, pg_temp;The Edit function updates both the parent document and the specialized row:
CREATE OR REPLACE FUNCTION EditSensor (
pId uuid,
pParent uuid default null,
pType uuid default null,
pCode text default null,
pLabel text default null,
pDescription text default null,
pValue numeric default null,
pUnit text default null,
pMetadata jsonb default null
) RETURNS void
AS $$
DECLARE
uMethod uuid;
BEGIN
-- 1. Update parent document
PERFORM EditDocument(pId, pParent, pType, pLabel, pDescription, pDescription, current_locale());
-- 2. Update specialized row with coalesce to preserve existing values
UPDATE db.sensor
SET code = coalesce(pCode, code),
value = coalesce(pValue, value),
unit = coalesce(pUnit, unit),
metadata = CheckNull(coalesce(pMetadata, metadata, jsonb_build_object()))
WHERE id = pId;
-- 3. Execute the 'edit' method (fires events)
uMethod := GetMethod(GetObjectClass(pId), GetAction('edit'));
PERFORM ExecuteMethod(pId, uMethod);
END;
$$ LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = kernel, pg_temp;Key pattern: use coalesce(pNewValue, existingColumn) so that passing NULL for a parameter means "keep the current value".
Use CheckNull() to convert empty strings or empty JSON to actual NULL when you want to allow clearing a field by passing an empty value.
The ObjectSensor view joins the full metadata chain. For Document entities, this always includes:
FROM db.sensor t INNER JOIN db.document d ON t.document = d.id -- document data
LEFT JOIN db.document_text dt ON dt.document = d.id ... -- localized description
INNER JOIN db.object o ON t.document = o.id -- object metadata
LEFT JOIN db.object_text ot ON ot.object = o.id ... -- localized label
INNER JOIN db.entity e ON o.entity = e.id -- entity info
LEFT JOIN db.entity_text et ON ...
INNER JOIN db.class_tree ct ON o.class = ct.id -- class info
LEFT JOIN db.class_text ctt ON ...
INNER JOIN db.type y ON o.type = y.id -- type info
LEFT JOIN db.type_text ty ON ...
INNER JOIN db.state_type st ON o.state_type = st.id -- state type
LEFT JOIN db.state_type_text stt ON ...
INNER JOIN db.state s ON o.state = s.id -- current state
LEFT JOIN db.state_text sst ON ...
INNER JOIN db.user w ON o.owner = w.id -- owner
INNER JOIN db.user u ON o.oper = u.id -- last operator
INNER JOIN DocumentAreaTree a ON d.area = a.id -- area (multi-tenant)
INNER JOIN db.scope sc ON o.scope = sc.id; -- scopeThe DocumentAreaTree join is specific to Documents and provides hierarchical area filtering.
Documents belong to an area (organizational unit / tenant). When a document is created, it is automatically assigned to the current session's area. Users can only see documents in areas they are members of.
The DocumentAreaTree view provides hierarchical area access -- if a user has access to a parent area, they can see documents in child areas.
To change a document's area:
PERFORM ChangeDocumentArea(uDocumentId, uNewAreaId);You can create entity hierarchies beyond the two-level pattern. For example, in Campus CORS:
document (abstract, Platform)
-> device (abstract, Configuration)
-> station (concrete, Configuration)
-> caster (concrete, Configuration)
-> client (concrete, Configuration)
-> employee (concrete, Configuration)
-> customer (concrete, Configuration)
-> partner (concrete, Configuration)
When creating a sub-class:
- Your entity's table references the parent entity's table (not
db.documentdirectly) - Your
CreateEntity*function gets the parent entity rather than creating a new one - Your
CreateClass*function creates the class under the parent class
Example from Campus CORS -- Station is a sub-class of Device:
CREATE OR REPLACE FUNCTION CreateEntityStation (
pParent uuid -- This is GetClass('device'), not GetClass('document')
)
RETURNS uuid
AS $$
DECLARE
uEntity uuid;
BEGIN
-- Reuse the 'device' entity (don't create a new one)
uEntity := GetEntity('device');
-- Create class under the device class
PERFORM CreateClassStation(pParent, uEntity);
-- API route
PERFORM RegisterRoute('station', AddEndpoint('SELECT * FROM rest.station($1, $2);'));
RETURN uEntity;
END
$$ LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = kernel, pg_temp;And CreateStation calls CreateDevice (not CreateDocument directly):
uDevice := CreateDevice(pParent, pType, pModel, pClient, ...);
INSERT INTO db.station (id, device, navigation, ...)
VALUES (uDevice, uDevice, pNavigation, ...);For the default 4-state lifecycle, use AddDefaultMethods(uClass) in your init.sql.
For custom workflows with additional states, see 74-Workflow-Customization. The Station entity is a good example -- it adds custom states available, unavailable, and faulted within the enabled state type.
Concepts
API Guide
Authentication & Session
- Connection
- Registration
- Authorization (OAuth 2.0)
- Sign In
- Sign Out
- Password Recovery
- Verification Codes
- Authentication
- Authorization
- Who Am I?
Core Services
Object & Workflow Endpoints
Schema & Internals
Configuration Developer Guide
- Configuration Guide
- Creating an Entity
- Creating a Document
- Creating a Reference
- Workflow Customization
- REST Endpoint Guide
- Event Handler Guide
Operations