
Let a connection carry its own information
A membership can have a role and a start date. A connection between two tasks can mean that one blocks the other. The relationship itself contains information worth keeping.
Wildo can represent that connection as a resource. It has its own fields and operations while linking the records on either side, so the information about the connection has a clear home.
The same approach supports connections between records of the same kind. Explicit source and target fields make the direction of a task dependency understandable.
Example — Explain why two tasks are connected
“Prepare the launch” depends on “Approve the brief”. A connection record identifies both tasks and records the kind of relationship, instead of leaving the dependency hidden in a comment or an unexplained pair of IDs.
For engineers
Store facts about the connection
A junction resource represents the link, not either endpoint. Its fields can describe the role, status or type of the connection. The junction has its own resource configuration and access choices.
Wonder Todos models connections between tasks in todos-relationships.schemas.ts. This excerpt includes the fields and the check that the two ends differ:
export const TodosRelationships_BaseSchema = z.object({
_id: z.string().min(1).isPrimaryKey().isSummaryField(),
organizationId: z.string().min(1).isDBIndexed().isForeignKey().isSummaryField().excludeFromUpdate(),
createdByUserId: z.string().min(1).isDBIndexed().isForeignKey().excludeFromUpdate(),
todoId: z.string().min(1).isDBIndexed().isForeignKey().isSummaryField().excludeFromUpdate(),
targetTodoId: z.string().min(1).isDBIndexed().isForeignKey().isSummaryField().excludeFromUpdate(),
relationshipType: z.enum(TodosRelationships_Type).isSummaryField(),
createdAt: z.date().isDBIndexed().isSummaryField().excludeFromCreate().excludeFromUpdate(),
updatedAt: z.date().isDBIndexed().excludeFromCreate().excludeFromUpdate()
});
export const TodosRelationships_Schema = TodosRelationships_BaseSchema.refine(
(data) => data.todoId !== data.targetTodoId,
{
message: "Source and target todos cannot be the same",
path: ["targetTodoId"]
}
);
todoId identifies the source task and targetTodoId the other task. relationshipType uses the application’s named vocabulary, which includes dependency and blocking relationships. The refinement attaches a same-task error to the target field so the invalid choice has a useful location.
Register both ends of a self-relationship
Two foreign keys alone do not tell the graph which task is the source, which is the target, or where the connection belongs. Wonder Todos declares three edges in tasks-manager.relationships.ts: an organization scope anchor, a source composition and a target reference. These declarations retain the actual options; source comments are omitted.
createResourcesRelationship(
CoreResourceType.ORGANIZATIONS, TasksManager_ResourceType.TODOS_RELATIONSHIPS,
ResourceRelationshipCardinality.ONE, ResourceRelationshipCardinality.MANY,
{
nature: RelationshipNature.COMPOSITION,
isPrimaryScope: true,
foreignKeyField: 'organizationId',
contextPolicy: {}
}
),
createResourcesRelationship(
TasksManager_ResourceType.TODOS, TasksManager_ResourceType.TODOS_RELATIONSHIPS,
ResourceRelationshipCardinality.ONE, ResourceRelationshipCardinality.MANY,
{
nature: RelationshipNature.COMPOSITION,
foreignKeyField: 'todoId',
allowCycle: true,
accessScopeStrategy: ResourceRelationshipAccessScopeStrategy.OPTIONAL_CONTEXT,
contextPolicy: {
objectMode: ContextPolicy_ObjectMode.ID_ONLY,
depth: 1,
}
}
),
createResourcesRelationship(
TasksManager_ResourceType.TODOS, TasksManager_ResourceType.TODOS_RELATIONSHIPS,
ResourceRelationshipCardinality.ONE, ResourceRelationshipCardinality.MANY,
{
nature: RelationshipNature.REFERENCE,
foreignKeyField: 'targetTodoId',
allowCycle: true,
accessScopeStrategy: ResourceRelationshipAccessScopeStrategy.OPTIONAL_CONTEXT,
contextPolicy: {
objectMode: ContextPolicy_ObjectMode.ID_ONLY,
depth: 1,
}
}
),
| Edge | Field on the connection | Purpose |
|---|---|---|
| Organization → connection | organizationId | Establishes the primary organization scope of the link record. |
| Source task → connection | todoId | Names the task that owns this connection. |
| Target task → connection | targetTodoId | References the other task without making it the owner. |
The two task edges use different field identities even though they reach the same resource type. allowCycle: true admits this declared graph topology; it is not a promise that a dependency scheduler will detect or accept business cycles. Their ID_ONLY context and depth of one bound related-object expansion.
Bind the checked schema to the resource
The connection’s resource configuration uses the refined schema, not the unrefined base. Selected lines from todos-relationships.resources-config.ts show the binding:
createResourceConfiguration_Initialization({
mainSchema: TodosRelationships_Schema,
resourceIdentifier: TasksManager_ResourceType.TODOS_RELATIONSHIPS,
resourceFieldIdentifier: TasksManager_ResourceFieldIdentifier[TasksManager_ResourceType.TODOS_RELATIONSHIPS],
resourceRelationships: resourcesRelationships,
// Other resource and operation settings are omitted.
});
This connects the source/target check to the derived write contract and gives the factory the graph it must interpret. Keep the field names, registered edges and the configuration’s schema in agreement. A source-equals-target error is one local rule; it does not prove that a longer chain of dependencies has no cycle.
Use a peer association for different resource types
Organization membership is a different setup: two distinct peer types connected through a named junction. The engine declares this association in resources-registry.shared.core.definitions.ts:
createResourcesRelationship(
CoreResourceType.ORGANIZATIONS, CoreResourceType.USERS,
ResourceRelationshipCardinality.MANY, ResourceRelationshipCardinality.MANY,
{
jointResourceType: CoreResourceType.ORGANIZATION_MEMBERS,
accessScopeStrategy: ResourceRelationshipAccessScopeStrategy.STANDALONE,
nature: RelationshipNature.ASSOCIATION,
contextPolicy: {
objectMode: ContextPolicy_ObjectMode.SUMMARY,
operationOverrides: {
[CoreResourceOperation.LIST]: { enabled: false },
[CoreResourceOperation.SEARCH]: { enabled: false },
}
}
}
),
jointResourceType identifies the membership resource that carries each connection’s fields and operations. The registered association supplies the peer expansion; merely adding organization and user IDs to an unrelated schema does not.
Do not reuse this single many-to-many declaration with the same task type at both ends. Self-referencing peer expansion is refused; the explicit source and target edges above describe the junction record directly and preserve which foreign key means what.
Decide what belongs to the link
Put connection-specific facts on the junction. A person’s organization role belongs to membership, while a task’s priority belongs to the task. Give the junction its own operations and lifecycle where people need to create, change or remove the connection.
A relationship type named “depends on” records that meaning; any scheduling or completion rule that acts on it belongs to the application’s behavior. The schema above also checks that the two task IDs differ. It does not, by itself, define a complete scheduling algorithm.