Skip to content

72 Creating Document

Преподобный Ален edited this page Feb 18, 2026 · 1 revision

Creating a Document Entity

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.

Document vs Reference

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

Table Pattern for Documents

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.

Optional: BEFORE UPDATE trigger for access control

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();

Real-World Example: Station (from Campus CORS)

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.

CreateDocument -- The Parent Factory

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 in db.object_text)
  • pDescription -- description (stored in db.document_text)

CreateDocument internally:

  1. Calls CreateObject() which inserts into db.object
  2. Inserts into db.document with the current area and priority
  3. 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);

Complete Create Function Example

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;

Edit Function Pattern

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.

ObjectView Pattern for Documents

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;          -- scope

The DocumentAreaTree join is specific to Documents and provides hierarchical area filtering.

Area-Based Access Control

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);

Sub-Classing Documents

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:

  1. Your entity's table references the parent entity's table (not db.document directly)
  2. Your CreateEntity* function gets the parent entity rather than creating a new one
  3. 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, ...);

Document State Machine Customization

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.

Clone this wiki locally