allow adding children records
This commit is contained in:
parent
225a3e8f75
commit
27dd83f488
@ -1,5 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, reactive, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { auth } from '@/auth'
|
import { auth } from '@/auth'
|
||||||
|
|
||||||
@ -19,6 +19,7 @@
|
|||||||
interface ChildRecords {
|
interface ChildRecords {
|
||||||
entity: Entity
|
entity: Entity
|
||||||
relationshipField: Field
|
relationshipField: Field
|
||||||
|
relationshipDirection?: 'INBOUND' | 'OUTBOUND'
|
||||||
records: Record<string, unknown>[]
|
records: Record<string, unknown>[]
|
||||||
}
|
}
|
||||||
interface EntityRecord {
|
interface EntityRecord {
|
||||||
@ -58,7 +59,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function childKey (child: ChildRecords) {
|
function childKey (child: ChildRecords) {
|
||||||
return `${child.entity.id}-${child.relationshipField.id}`
|
return `${child.entity.id}-${child.relationshipField.id}-${child.relationshipDirection ?? 'INBOUND'}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function editableChildFields (child: ChildRecords) {
|
function editableChildFields (child: ChildRecords) {
|
||||||
@ -99,6 +100,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadRecord () {
|
async function loadRecord () {
|
||||||
|
loading.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
editing.value = false
|
||||||
|
addingChildKey.value = ''
|
||||||
try {
|
try {
|
||||||
const response = await fetch(recordUrl.value, {
|
const response = await fetch(recordUrl.value, {
|
||||||
headers: { Authorization: `Bearer ${auth.token}` },
|
headers: { Authorization: `Bearer ${auth.token}` },
|
||||||
@ -146,17 +151,19 @@
|
|||||||
saving.value = true
|
saving.value = true
|
||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const childUrl = new URL(
|
||||||
`${recordUrl.value}/children/${encodeURIComponent(child.entity.name)}/${child.relationshipField.id}`,
|
`${recordUrl.value}/children/${encodeURIComponent(child.entity.name)}/${child.relationshipField.id}`,
|
||||||
{
|
window.location.origin,
|
||||||
|
)
|
||||||
|
childUrl.searchParams.set('direction', child.relationshipDirection ?? 'INBOUND')
|
||||||
|
const response = await fetch(childUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${auth.token}`,
|
'Authorization': `Bearer ${auth.token}`,
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
},
|
||||||
body: formBody(fields, childValues[key]),
|
body: formBody(fields, childValues[key]),
|
||||||
},
|
})
|
||||||
)
|
|
||||||
if (!response.ok) throw new Error('Child create failed')
|
if (!response.ok) throw new Error('Child create failed')
|
||||||
const created = await response.json() as EntityRecord
|
const created = await response.json() as EntityRecord
|
||||||
child.records.push(created.values)
|
child.records.push(created.values)
|
||||||
@ -168,7 +175,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(loadRecord)
|
watch(recordUrl, loadRecord, { immediate: true })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@ -14,6 +14,8 @@ test('admin opens a user file and views a linked entity record', async ({ page }
|
|||||||
let projectTitle = 'Website refresh'
|
let projectTitle = 'Website refresh'
|
||||||
const projects: Record<string, unknown>[] = [{ id: 42, title: 'Website refresh', owner: 7 }]
|
const projects: Record<string, unknown>[] = [{ id: 42, title: 'Website refresh', owner: 7 }]
|
||||||
const tasks: Record<string, unknown>[] = []
|
const tasks: Record<string, unknown>[] = []
|
||||||
|
const notes: Record<string, unknown>[] = []
|
||||||
|
const milestones: Record<string, unknown>[] = []
|
||||||
const projectResponse = () => ({
|
const projectResponse = () => ({
|
||||||
entity: {
|
entity: {
|
||||||
id: 3,
|
id: 3,
|
||||||
@ -22,6 +24,14 @@ test('admin opens a user file and views a linked entity record', async ({ page }
|
|||||||
fields: [
|
fields: [
|
||||||
{ id: 11, name: 'Title', identifier: 'title', type: 'TEXT' },
|
{ id: 11, name: 'Title', identifier: 'title', type: 'TEXT' },
|
||||||
{ id: 12, name: 'Owner', identifier: 'owner', type: 'USER' },
|
{ id: 12, name: 'Owner', identifier: 'owner', type: 'USER' },
|
||||||
|
{
|
||||||
|
id: 13,
|
||||||
|
name: 'Milestones',
|
||||||
|
identifier: 'milestones',
|
||||||
|
type: 'RELATIONSHIP',
|
||||||
|
targetEntityId: 6,
|
||||||
|
relationshipType: 'ONE_TO_MANY',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
values: { id: 42, title: projectTitle, owner: 7 },
|
values: { id: 42, title: projectTitle, owner: 7 },
|
||||||
@ -49,6 +59,23 @@ test('admin opens a user file and views a linked entity record', async ({ page }
|
|||||||
targetEntityId: 3,
|
targetEntityId: 3,
|
||||||
},
|
},
|
||||||
records: tasks,
|
records: tasks,
|
||||||
|
}, {
|
||||||
|
entity: {
|
||||||
|
id: 6,
|
||||||
|
name: 'Milestone',
|
||||||
|
identifier: 'milestone',
|
||||||
|
fields: [{ id: 40, name: 'Label', identifier: 'label', type: 'TEXT' }],
|
||||||
|
},
|
||||||
|
relationshipField: {
|
||||||
|
id: 13,
|
||||||
|
name: 'Milestones',
|
||||||
|
identifier: 'milestones',
|
||||||
|
type: 'RELATIONSHIP',
|
||||||
|
targetEntityId: 6,
|
||||||
|
relationshipType: 'ONE_TO_MANY',
|
||||||
|
},
|
||||||
|
relationshipDirection: 'OUTBOUND',
|
||||||
|
records: milestones,
|
||||||
}],
|
}],
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -90,7 +117,7 @@ test('admin opens a user file and views a linked entity record', async ({ page }
|
|||||||
}
|
}
|
||||||
await route.fulfill({ json: projectResponse() })
|
await route.fulfill({ json: projectResponse() })
|
||||||
})
|
})
|
||||||
await page.route('**/api/users/7/entities/Project/records/42/children/Task/21', async route => {
|
await page.route('**/api/users/7/entities/Project/records/42/children/Task/21*', async route => {
|
||||||
const values = new URLSearchParams(route.request().postData() ?? '')
|
const values = new URLSearchParams(route.request().postData() ?? '')
|
||||||
const created = { id: 55, description: values.get('description'), project: 42 }
|
const created = { id: 55, description: values.get('description'), project: 42 }
|
||||||
tasks.push(created)
|
tasks.push(created)
|
||||||
@ -103,6 +130,50 @@ test('admin opens a user file and views a linked entity record', async ({ page }
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
await page.route('**/api/users/7/entities/Project/records/42/children/Milestone/13*', async route => {
|
||||||
|
expect(new URL(route.request().url()).searchParams.get('direction')).toBe('OUTBOUND')
|
||||||
|
const values = new URLSearchParams(route.request().postData() ?? '')
|
||||||
|
const created = { id: 60, label: values.get('label') }
|
||||||
|
milestones.push(created)
|
||||||
|
await route.fulfill({
|
||||||
|
status: 201,
|
||||||
|
json: { entity: projectResponse().children.at(1)!.entity, values: created, children: [] },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
await page.route('**/api/users/7/entities/Task/records/55', route => route.fulfill({
|
||||||
|
json: {
|
||||||
|
entity: projectResponse().children.at(0)!.entity,
|
||||||
|
values: tasks.at(0),
|
||||||
|
children: [{
|
||||||
|
entity: {
|
||||||
|
id: 5,
|
||||||
|
name: 'Note',
|
||||||
|
identifier: 'note',
|
||||||
|
fields: [
|
||||||
|
{ id: 30, name: 'Contents', identifier: 'contents', type: 'TEXT' },
|
||||||
|
{ id: 31, name: 'Task', identifier: 'task', type: 'RELATIONSHIP', targetEntityId: 4 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
relationshipField: {
|
||||||
|
id: 31,
|
||||||
|
name: 'Task',
|
||||||
|
identifier: 'task',
|
||||||
|
type: 'RELATIONSHIP',
|
||||||
|
targetEntityId: 4,
|
||||||
|
},
|
||||||
|
records: notes,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
await page.route('**/api/users/7/entities/Task/records/55/children/Note/31*', async route => {
|
||||||
|
const values = new URLSearchParams(route.request().postData() ?? '')
|
||||||
|
const created = { id: 56, contents: values.get('contents'), task: 55 }
|
||||||
|
notes.push(created)
|
||||||
|
await route.fulfill({
|
||||||
|
status: 201,
|
||||||
|
json: { entity: { id: 5, name: 'Note', fields: [] }, values: created, children: [] },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
await page.goto('/users')
|
await page.goto('/users')
|
||||||
await page.getByRole('link', { name: 'alex' }).click()
|
await page.getByRole('link', { name: 'alex' }).click()
|
||||||
@ -123,6 +194,12 @@ test('admin opens a user file and views a linked entity record', async ({ page }
|
|||||||
await expect(page.getByText('Website refresh')).toBeVisible()
|
await expect(page.getByText('Website refresh')).toBeVisible()
|
||||||
await expect(page.getByText('Owner')).toBeVisible()
|
await expect(page.getByText('Owner')).toBeVisible()
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Add Milestone' }).click()
|
||||||
|
const milestoneForm = page.getByRole('form', { name: 'Add Milestone' })
|
||||||
|
await milestoneForm.getByLabel('Label').fill('Public beta')
|
||||||
|
await milestoneForm.getByRole('button', { name: 'Add Milestone' }).click()
|
||||||
|
await expect(page.getByRole('row').filter({ hasText: 'Public beta' })).toBeVisible()
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Edit record' }).click()
|
await page.getByRole('button', { name: 'Edit record' }).click()
|
||||||
const editForm = page.getByRole('form', { name: 'Edit record' })
|
const editForm = page.getByRole('form', { name: 'Edit record' })
|
||||||
await editForm.getByLabel('Title').fill('Mobile application')
|
await editForm.getByLabel('Title').fill('Mobile application')
|
||||||
@ -134,4 +211,12 @@ test('admin opens a user file and views a linked entity record', async ({ page }
|
|||||||
await childForm.getByLabel('Description').fill('Prepare launch')
|
await childForm.getByLabel('Description').fill('Prepare launch')
|
||||||
await childForm.getByRole('button', { name: 'Add Task' }).click()
|
await childForm.getByRole('button', { name: 'Add Task' }).click()
|
||||||
await expect(page.getByRole('row').filter({ hasText: 'Prepare launch' })).toContainText('42')
|
await expect(page.getByRole('row').filter({ hasText: 'Prepare launch' })).toContainText('42')
|
||||||
|
|
||||||
|
await page.getByRole('link', { name: 'View Task record 55' }).click()
|
||||||
|
await expect(page.getByRole('heading', { name: 'Task #55' })).toBeVisible()
|
||||||
|
await page.getByRole('button', { name: 'Add Note' }).click()
|
||||||
|
const noteForm = page.getByRole('form', { name: 'Add Note' })
|
||||||
|
await noteForm.getByLabel('Contents').fill('Nested child')
|
||||||
|
await noteForm.getByRole('button', { name: 'Add Note' }).click()
|
||||||
|
await expect(page.getByRole('row').filter({ hasText: 'Nested child' })).toContainText('55')
|
||||||
})
|
})
|
||||||
|
|||||||
@ -22,8 +22,11 @@ data class ChildEntityRecords(
|
|||||||
val entity: EntityDefinition,
|
val entity: EntityDefinition,
|
||||||
val relationshipField: EntityField,
|
val relationshipField: EntityField,
|
||||||
val records: List<Map<String, Any?>>,
|
val records: List<Map<String, Any?>>,
|
||||||
|
val relationshipDirection: RelationshipDirection = RelationshipDirection.INBOUND,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
enum class RelationshipDirection { INBOUND, OUTBOUND }
|
||||||
|
|
||||||
class EntityRecordRepository(
|
class EntityRecordRepository(
|
||||||
private val database: Database,
|
private val database: Database,
|
||||||
private val entities: EntityDefinitionRepository,
|
private val entities: EntityDefinitionRepository,
|
||||||
@ -109,12 +112,42 @@ class EntityRecordRepository(
|
|||||||
parentRecordId: Long,
|
parentRecordId: Long,
|
||||||
childEntityName: String,
|
childEntityName: String,
|
||||||
relationshipFieldId: Long,
|
relationshipFieldId: Long,
|
||||||
|
relationshipDirection: RelationshipDirection,
|
||||||
submittedValues: Map<String, String>,
|
submittedValues: Map<String, String>,
|
||||||
): EntityRecord? {
|
): EntityRecord? {
|
||||||
val definitions = entities.findAll()
|
val definitions = entities.findAll()
|
||||||
val parent = definitions.firstOrNull { it.name == parentEntityName } ?: return null
|
val parent = definitions.firstOrNull { it.name == parentEntityName } ?: return null
|
||||||
if (findRecordLinkedToUser(userId, parentEntityName, parentRecordId) == null) return null
|
if (findRecordLinkedToUser(userId, parentEntityName, parentRecordId) == null) return null
|
||||||
val child = definitions.firstOrNull { it.name == childEntityName } ?: return null
|
val child = definitions.firstOrNull { it.name == childEntityName } ?: return null
|
||||||
|
if (relationshipDirection == RelationshipDirection.OUTBOUND) {
|
||||||
|
val relationship = parent.fields.firstOrNull {
|
||||||
|
it.id == relationshipFieldId && it.type == FieldType.RELATIONSHIP && it.targetEntityId == child.id
|
||||||
|
} ?: return null
|
||||||
|
val storedTargetId = findValues(parent, parentRecordId)?.get(relationship.identifier) as? Number
|
||||||
|
if (
|
||||||
|
relationship.relationshipType == RelationshipType.ONE_TO_ONE &&
|
||||||
|
(storedTargetId != null || outboundChildIds(relationship.id, parentRecordId).isNotEmpty())
|
||||||
|
) {
|
||||||
|
throw IllegalArgumentException("This relationship already has a child")
|
||||||
|
}
|
||||||
|
val childId = insertRecord(child, userId, submittedValues)
|
||||||
|
database.executeUpdate(
|
||||||
|
"INSERT INTO entity_record_relationships (field_id, source_record_id, target_record_id) VALUES (?, ?, ?)",
|
||||||
|
) {
|
||||||
|
setLong(1, relationship.id)
|
||||||
|
setLong(2, parentRecordId)
|
||||||
|
setLong(3, childId)
|
||||||
|
}
|
||||||
|
if (relationship.relationshipType == RelationshipType.ONE_TO_ONE) {
|
||||||
|
database.executeUpdate(
|
||||||
|
"UPDATE ${quote(parent.identifier)} SET ${quote(relationship.identifier)} = ? WHERE \"id\" = ?",
|
||||||
|
) {
|
||||||
|
setLong(1, childId)
|
||||||
|
setLong(2, parentRecordId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return findChildRecord(child, childId)
|
||||||
|
}
|
||||||
val parentField = child.fields.firstOrNull {
|
val parentField = child.fields.firstOrNull {
|
||||||
it.id == relationshipFieldId && it.type == FieldType.RELATIONSHIP && it.targetEntityId == parent.id
|
it.id == relationshipFieldId && it.type == FieldType.RELATIONSHIP && it.targetEntityId == parent.id
|
||||||
} ?: return null
|
} ?: return null
|
||||||
@ -133,8 +166,9 @@ class EntityRecordRepository(
|
|||||||
return findChildRecord(child, childId)
|
return findChildRecord(child, childId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findChildren(parentEntityId: Long, parentRecordId: Long): List<ChildEntityRecords> =
|
private fun findChildren(parentEntityId: Long, parentRecordId: Long): List<ChildEntityRecords> {
|
||||||
entities.findAll().flatMap { child ->
|
val definitions = entities.findAll()
|
||||||
|
val inbound = definitions.flatMap { child ->
|
||||||
child.fields
|
child.fields
|
||||||
.filter { it.type == FieldType.RELATIONSHIP && it.targetEntityId == parentEntityId }
|
.filter { it.type == FieldType.RELATIONSHIP && it.targetEntityId == parentEntityId }
|
||||||
.map { relationship ->
|
.map { relationship ->
|
||||||
@ -147,6 +181,40 @@ class EntityRecordRepository(
|
|||||||
ChildEntityRecords(child, relationship, records)
|
ChildEntityRecords(child, relationship, records)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val parent = definitions.firstOrNull { it.id == parentEntityId } ?: return inbound
|
||||||
|
val outbound = parent.fields
|
||||||
|
.filter { it.type == FieldType.RELATIONSHIP && it.targetEntityId != null }
|
||||||
|
.mapNotNull { relationship ->
|
||||||
|
val child = definitions.firstOrNull { it.id == relationship.targetEntityId } ?: return@mapNotNull null
|
||||||
|
val storedTargetId = findValues(parent, parentRecordId)?.get(relationship.identifier)
|
||||||
|
?.let { it as? Number }?.toLong()
|
||||||
|
val childIds = (outboundChildIds(relationship.id, parentRecordId) + listOfNotNull(storedTargetId)).distinct()
|
||||||
|
val records = childIds.mapNotNull { findValues(child, it) }
|
||||||
|
ChildEntityRecords(child, relationship, records, RelationshipDirection.OUTBOUND)
|
||||||
|
}
|
||||||
|
return inbound + outbound
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun outboundChildIds(fieldId: Long, sourceRecordId: Long): List<Long> =
|
||||||
|
database.queryList(
|
||||||
|
"SELECT target_record_id FROM entity_record_relationships WHERE field_id = ? AND source_record_id = ? ORDER BY target_record_id",
|
||||||
|
bind = {
|
||||||
|
setLong(1, fieldId)
|
||||||
|
setLong(2, sourceRecordId)
|
||||||
|
},
|
||||||
|
) { getLong("target_record_id") }
|
||||||
|
|
||||||
|
private fun insertRecord(entity: EntityDefinition, userId: Long, submittedValues: Map<String, String>): Long {
|
||||||
|
val fields = entity.fields
|
||||||
|
val sql = "INSERT INTO ${quote(entity.identifier)} (${fields.joinToString { quote(it.identifier) }}) " +
|
||||||
|
"VALUES (${fields.joinToString { "?" }}) RETURNING \"id\""
|
||||||
|
return checkNotNull(database.queryOne(sql, bind = {
|
||||||
|
fields.forEachIndexed { index, field ->
|
||||||
|
if (field.type == FieldType.USER) setLong(index + 1, userId)
|
||||||
|
else bindField(index + 1, field, submittedValues[field.identifier].orEmpty())
|
||||||
|
}
|
||||||
|
}) { getLong("id") })
|
||||||
|
}
|
||||||
|
|
||||||
private fun findChildRecord(entity: EntityDefinition, recordId: Long): EntityRecord? {
|
private fun findChildRecord(entity: EntityDefinition, recordId: Long): EntityRecord? {
|
||||||
val values = findValues(entity, recordId) ?: return null
|
val values = findValues(entity, recordId) ?: return null
|
||||||
@ -173,7 +241,7 @@ class EntityRecordRepository(
|
|||||||
if (entity.fields.any { it.type == FieldType.USER && (values[it.identifier] as? Number)?.toLong() == userId }) {
|
if (entity.fields.any { it.type == FieldType.USER && (values[it.identifier] as? Number)?.toLong() == userId }) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return entity.fields
|
val accessibleThroughStoredField = entity.fields
|
||||||
.filter { it.type == FieldType.RELATIONSHIP && it.targetEntityId != null }
|
.filter { it.type == FieldType.RELATIONSHIP && it.targetEntityId != null }
|
||||||
.any { field ->
|
.any { field ->
|
||||||
val parentId = (values[field.identifier] as? Number)?.toLong() ?: return@any false
|
val parentId = (values[field.identifier] as? Number)?.toLong() ?: return@any false
|
||||||
@ -181,6 +249,24 @@ class EntityRecordRepository(
|
|||||||
val parentValues = findValues(parent, parentId) ?: return@any false
|
val parentValues = findValues(parent, parentId) ?: return@any false
|
||||||
isAccessibleToUser(parent, parentValues, userId, definitions, visited)
|
isAccessibleToUser(parent, parentValues, userId, definitions, visited)
|
||||||
}
|
}
|
||||||
|
if (accessibleThroughStoredField) return true
|
||||||
|
return definitions.any { parent ->
|
||||||
|
parent.fields
|
||||||
|
.filter { it.type == FieldType.RELATIONSHIP && it.targetEntityId == entity.id }
|
||||||
|
.any { field ->
|
||||||
|
database.queryList(
|
||||||
|
"SELECT source_record_id FROM entity_record_relationships " +
|
||||||
|
"WHERE field_id = ? AND target_record_id = ?",
|
||||||
|
bind = {
|
||||||
|
setLong(1, field.id)
|
||||||
|
setLong(2, recordId)
|
||||||
|
},
|
||||||
|
) { getLong("source_record_id") }.any { parentId ->
|
||||||
|
val parentValues = findValues(parent, parentId) ?: return@any false
|
||||||
|
isAccessibleToUser(parent, parentValues, userId, definitions, visited)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun PreparedStatement.bindField(index: Int, field: EntityField, rawValue: String) {
|
private fun PreparedStatement.bindField(index: Int, field: EntityField, rawValue: String) {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package dev.mduchene.bolts.web
|
package dev.mduchene.bolts.web
|
||||||
|
|
||||||
import dev.mduchene.bolts.entity.EntityRecordRepository
|
import dev.mduchene.bolts.entity.EntityRecordRepository
|
||||||
|
import dev.mduchene.bolts.entity.RelationshipDirection
|
||||||
import dev.mduchene.bolts.user.LoginService
|
import dev.mduchene.bolts.user.LoginService
|
||||||
import dev.mduchene.bolts.user.UserRepository
|
import dev.mduchene.bolts.user.UserRepository
|
||||||
import io.javalin.router.JavalinDefaultRoutingApi
|
import io.javalin.router.JavalinDefaultRoutingApi
|
||||||
@ -114,6 +115,9 @@ class UserController(
|
|||||||
recordId,
|
recordId,
|
||||||
childEntityName,
|
childEntityName,
|
||||||
fieldId,
|
fieldId,
|
||||||
|
ctx.queryParam("direction")?.let {
|
||||||
|
runCatching { RelationshipDirection.valueOf(it) }.getOrNull()
|
||||||
|
} ?: RelationshipDirection.INBOUND,
|
||||||
ctx.singleFormParams(),
|
ctx.singleFormParams(),
|
||||||
)
|
)
|
||||||
if (child == null) ctx.notFound() else ctx.status(201).json(child)
|
if (child == null) ctx.notFound() else ctx.status(201).json(child)
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE entity_record_relationships (
|
||||||
|
field_id BIGINT NOT NULL REFERENCES entity_fields(id) ON DELETE CASCADE,
|
||||||
|
source_record_id BIGINT NOT NULL,
|
||||||
|
target_record_id BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (field_id, source_record_id, target_record_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX entity_record_relationships_target_idx
|
||||||
|
ON entity_record_relationships(field_id, target_record_id);
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
DROP TABLE IF EXISTS entity_record_relationships;
|
||||||
DROP TABLE IF EXISTS entity_fields;
|
DROP TABLE IF EXISTS entity_fields;
|
||||||
DROP TABLE IF EXISTS entity_definitions;
|
DROP TABLE IF EXISTS entity_definitions;
|
||||||
DROP TABLE IF EXISTS sessions;
|
DROP TABLE IF EXISTS sessions;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user