use entity name in routes + unique entity names
This commit is contained in:
parent
c6b1127e30
commit
c7566bf4a0
@ -39,7 +39,8 @@
|
|||||||
const addingChildKey = ref('')
|
const addingChildKey = ref('')
|
||||||
const userFilePath = computed(() => `/users/${route.params.userId}`)
|
const userFilePath = computed(() => `/users/${route.params.userId}`)
|
||||||
const recordUrl = computed(
|
const recordUrl = computed(
|
||||||
() => `/api/users/${route.params.userId}/entities/${route.params.entityId}/records/${route.params.recordId}`,
|
() => `/api/users/${route.params.userId}/entities/${encodeURIComponent(String(route.params.entityName))}`
|
||||||
|
+ `/records/${route.params.recordId}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
function displayValue (value: unknown) {
|
function displayValue (value: unknown) {
|
||||||
@ -146,7 +147,7 @@
|
|||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${recordUrl.value}/children/${child.entity.id}/${child.relationshipField.id}`,
|
`${recordUrl.value}/children/${encodeURIComponent(child.entity.name)}/${child.relationshipField.id}`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@ -305,7 +306,7 @@
|
|||||||
<router-link
|
<router-link
|
||||||
:aria-label="`View ${child.entity.name} record ${childRecord.id}`"
|
:aria-label="`View ${child.entity.name} record ${childRecord.id}`"
|
||||||
class="back-link"
|
class="back-link"
|
||||||
:to="`/users/${route.params.userId}/entities/${child.entity.id}/records/${childRecord.id}`"
|
:to="`/users/${route.params.userId}/entities/${encodeURIComponent(child.entity.name)}/records/${childRecord.id}`"
|
||||||
>
|
>
|
||||||
View
|
View
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|||||||
@ -103,7 +103,7 @@
|
|||||||
<router-link
|
<router-link
|
||||||
:aria-label="`View ${linked.entity.name} record ${record.id}`"
|
:aria-label="`View ${linked.entity.name} record ${record.id}`"
|
||||||
class="view-link"
|
class="view-link"
|
||||||
:to="`/users/${file.user.id}/entities/${linked.entity.id}/records/${record.id}`"
|
:to="`/users/${file.user.id}/entities/${encodeURIComponent(linked.entity.name)}/records/${record.id}`"
|
||||||
>
|
>
|
||||||
View
|
View
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|||||||
@ -31,7 +31,7 @@ const router = createRouter({
|
|||||||
meta: { requiresAdmin: true },
|
meta: { requiresAdmin: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/users/:userId/entities/:entityId/records/:recordId',
|
path: '/users/:userId/entities/:entityName/records/:recordId',
|
||||||
component: EntityRecordViewer,
|
component: EntityRecordViewer,
|
||||||
meta: { requiresAdmin: true },
|
meta: { requiresAdmin: true },
|
||||||
},
|
},
|
||||||
|
|||||||
@ -72,14 +72,14 @@ test('admin opens a user file and views a linked entity record', async ({ page }
|
|||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
await page.route('**/api/users/7/entities/3/records/42', async route => {
|
await page.route('**/api/users/7/entities/Project/records/42', async route => {
|
||||||
if (route.request().method() === 'PATCH') {
|
if (route.request().method() === 'PATCH') {
|
||||||
const values = new URLSearchParams(route.request().postData() ?? '')
|
const values = new URLSearchParams(route.request().postData() ?? '')
|
||||||
projectTitle = values.get('title') ?? projectTitle
|
projectTitle = values.get('title') ?? projectTitle
|
||||||
}
|
}
|
||||||
await route.fulfill({ json: projectResponse() })
|
await route.fulfill({ json: projectResponse() })
|
||||||
})
|
})
|
||||||
await page.route('**/api/users/7/entities/3/records/42/children/4/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)
|
||||||
@ -101,6 +101,7 @@ test('admin opens a user file and views a linked entity record', async ({ page }
|
|||||||
await expect(page.getByRole('row').filter({ hasText: 'Website refresh' })).toContainText('7')
|
await expect(page.getByRole('row').filter({ hasText: 'Website refresh' })).toContainText('7')
|
||||||
|
|
||||||
await page.getByRole('link', { name: 'View Project record 42' }).click()
|
await page.getByRole('link', { name: 'View Project record 42' }).click()
|
||||||
|
await expect(page).toHaveURL('/users/7/entities/Project/records/42')
|
||||||
await expect(page.getByRole('heading', { name: 'Project #42' })).toBeVisible()
|
await expect(page.getByRole('heading', { name: 'Project #42' })).toBeVisible()
|
||||||
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()
|
||||||
|
|||||||
@ -4,6 +4,21 @@ import dev.mduchene.bolts.persistence.Database
|
|||||||
import java.sql.ResultSet
|
import java.sql.ResultSet
|
||||||
|
|
||||||
class EntityDefinitionRepository(private val database: Database) {
|
class EntityDefinitionRepository(private val database: Database) {
|
||||||
|
fun nameExists(name: String, excludingId: Long? = null): Boolean =
|
||||||
|
database.queryOne(
|
||||||
|
"""
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1 FROM entity_definitions
|
||||||
|
WHERE lower(name) = lower(?) AND (? IS NULL OR id <> ?)
|
||||||
|
) AS found
|
||||||
|
""".trimIndent(),
|
||||||
|
bind = {
|
||||||
|
setString(1, name)
|
||||||
|
setObject(2, excludingId)
|
||||||
|
setObject(3, excludingId)
|
||||||
|
},
|
||||||
|
) { getBoolean("found") } == true
|
||||||
|
|
||||||
fun findAll(): List<EntityDefinition> {
|
fun findAll(): List<EntityDefinition> {
|
||||||
val fields = database.queryList(
|
val fields = database.queryList(
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -51,9 +51,9 @@ class EntityRecordRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findRecordLinkedToUser(userId: Long, entityId: Long, recordId: Long): EntityRecord? {
|
fun findRecordLinkedToUser(userId: Long, entityName: String, recordId: Long): EntityRecord? {
|
||||||
val definitions = entities.findAll()
|
val definitions = entities.findAll()
|
||||||
val entity = definitions.firstOrNull { it.id == entityId } ?: return null
|
val entity = definitions.firstOrNull { it.name == entityName } ?: return null
|
||||||
val values = findValues(entity, recordId) ?: return null
|
val values = findValues(entity, recordId) ?: return null
|
||||||
if (!isAccessibleToUser(entity, values, userId, definitions, mutableSetOf())) return null
|
if (!isAccessibleToUser(entity, values, userId, definitions, mutableSetOf())) return null
|
||||||
return EntityRecord(entity, values, findChildren(entity.id, recordId))
|
return EntityRecord(entity, values, findChildren(entity.id, recordId))
|
||||||
@ -61,11 +61,11 @@ class EntityRecordRepository(
|
|||||||
|
|
||||||
fun updateRecordLinkedToUser(
|
fun updateRecordLinkedToUser(
|
||||||
userId: Long,
|
userId: Long,
|
||||||
entityId: Long,
|
entityName: String,
|
||||||
recordId: Long,
|
recordId: Long,
|
||||||
submittedValues: Map<String, String>,
|
submittedValues: Map<String, String>,
|
||||||
): EntityRecord? {
|
): EntityRecord? {
|
||||||
val current = findRecordLinkedToUser(userId, entityId, recordId) ?: return null
|
val current = findRecordLinkedToUser(userId, entityName, recordId) ?: return null
|
||||||
val fields = current.entity.fields.filter { it.type != FieldType.USER && it.identifier in submittedValues }
|
val fields = current.entity.fields.filter { it.type != FieldType.USER && it.identifier in submittedValues }
|
||||||
if (fields.isNotEmpty()) {
|
if (fields.isNotEmpty()) {
|
||||||
val assignments = fields.joinToString { "${quote(it.identifier)} = ?" }
|
val assignments = fields.joinToString { "${quote(it.identifier)} = ?" }
|
||||||
@ -77,21 +77,23 @@ class EntityRecordRepository(
|
|||||||
setLong(fields.size + 1, recordId)
|
setLong(fields.size + 1, recordId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return findRecordLinkedToUser(userId, entityId, recordId)
|
return findRecordLinkedToUser(userId, entityName, recordId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createChild(
|
fun createChild(
|
||||||
userId: Long,
|
userId: Long,
|
||||||
parentEntityId: Long,
|
parentEntityName: String,
|
||||||
parentRecordId: Long,
|
parentRecordId: Long,
|
||||||
childEntityId: Long,
|
childEntityName: String,
|
||||||
relationshipFieldId: Long,
|
relationshipFieldId: Long,
|
||||||
submittedValues: Map<String, String>,
|
submittedValues: Map<String, String>,
|
||||||
): EntityRecord? {
|
): EntityRecord? {
|
||||||
if (findRecordLinkedToUser(userId, parentEntityId, parentRecordId) == null) return null
|
val definitions = entities.findAll()
|
||||||
val child = entities.findAll().firstOrNull { it.id == childEntityId } ?: return null
|
val parent = definitions.firstOrNull { it.name == parentEntityName } ?: return null
|
||||||
|
if (findRecordLinkedToUser(userId, parentEntityName, parentRecordId) == null) return null
|
||||||
|
val child = definitions.firstOrNull { it.name == childEntityName } ?: return null
|
||||||
val parentField = child.fields.firstOrNull {
|
val parentField = child.fields.firstOrNull {
|
||||||
it.id == relationshipFieldId && it.type == FieldType.RELATIONSHIP && it.targetEntityId == parentEntityId
|
it.id == relationshipFieldId && it.type == FieldType.RELATIONSHIP && it.targetEntityId == parent.id
|
||||||
} ?: return null
|
} ?: return null
|
||||||
val fields = child.fields
|
val fields = child.fields
|
||||||
val sql = "INSERT INTO ${quote(child.identifier)} (${fields.joinToString { quote(it.identifier) }}) " +
|
val sql = "INSERT INTO ${quote(child.identifier)} (${fields.joinToString { quote(it.identifier) }}) " +
|
||||||
|
|||||||
@ -33,6 +33,10 @@ class EntityDefinitionController(
|
|||||||
if (!ctx.requireAdmin(loginService)) return@post
|
if (!ctx.requireAdmin(loginService)) return@post
|
||||||
val name = ctx.requiredFormParam("name") ?: return@post
|
val name = ctx.requiredFormParam("name") ?: return@post
|
||||||
val identifier = ctx.requiredFormParam("identifier") ?: return@post
|
val identifier = ctx.requiredFormParam("identifier") ?: return@post
|
||||||
|
if (entities.nameExists(name)) {
|
||||||
|
ctx.badRequest("Entity names must be unique")
|
||||||
|
return@post
|
||||||
|
}
|
||||||
ctx.status(HttpStatus.CREATED).json(entities.create(name, identifier))
|
ctx.status(HttpStatus.CREATED).json(entities.create(name, identifier))
|
||||||
}
|
}
|
||||||
routes.patch("/api/entity-definitions/{entityId}") { ctx ->
|
routes.patch("/api/entity-definitions/{entityId}") { ctx ->
|
||||||
@ -41,6 +45,10 @@ class EntityDefinitionController(
|
|||||||
val name = ctx.requiredFormParam("name")
|
val name = ctx.requiredFormParam("name")
|
||||||
val identifier = ctx.requiredFormParam("identifier")
|
val identifier = ctx.requiredFormParam("identifier")
|
||||||
if (id == null || name == null || identifier == null) return@patch
|
if (id == null || name == null || identifier == null) return@patch
|
||||||
|
if (entities.nameExists(name, id)) {
|
||||||
|
ctx.badRequest("Entity names must be unique")
|
||||||
|
return@patch
|
||||||
|
}
|
||||||
val entity = entities.update(id, name, identifier)
|
val entity = entities.update(id, name, identifier)
|
||||||
if (entity == null) ctx.notFound() else ctx.json(entity)
|
if (entity == null) ctx.notFound() else ctx.json(entity)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,29 +38,29 @@ class UserController(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
routes.get("/api/users/{userId}/entities/{entityId}/records/{recordId}") { ctx ->
|
routes.get("/api/users/{userId}/entities/{entityName}/records/{recordId}") { ctx ->
|
||||||
if (!ctx.requireAdmin(loginService)) return@get
|
if (!ctx.requireAdmin(loginService)) return@get
|
||||||
val userId = ctx.pathParam("userId").toLongOrNull()
|
val userId = ctx.pathParam("userId").toLongOrNull()
|
||||||
val entityId = ctx.pathParam("entityId").toLongOrNull()
|
val entityName = ctx.pathParam("entityName")
|
||||||
val recordId = ctx.pathParam("recordId").toLongOrNull()
|
val recordId = ctx.pathParam("recordId").toLongOrNull()
|
||||||
if (userId == null || entityId == null || recordId == null || users.findById(userId) == null) {
|
if (userId == null || recordId == null || users.findById(userId) == null) {
|
||||||
ctx.notFound()
|
ctx.notFound()
|
||||||
return@get
|
return@get
|
||||||
}
|
}
|
||||||
val record = records.findRecordLinkedToUser(userId, entityId, recordId)
|
val record = records.findRecordLinkedToUser(userId, entityName, recordId)
|
||||||
if (record == null) ctx.notFound() else ctx.json(record)
|
if (record == null) ctx.notFound() else ctx.json(record)
|
||||||
}
|
}
|
||||||
routes.patch("/api/users/{userId}/entities/{entityId}/records/{recordId}") { ctx ->
|
routes.patch("/api/users/{userId}/entities/{entityName}/records/{recordId}") { ctx ->
|
||||||
if (!ctx.requireAdmin(loginService)) return@patch
|
if (!ctx.requireAdmin(loginService)) return@patch
|
||||||
val userId = ctx.pathParam("userId").toLongOrNull()
|
val userId = ctx.pathParam("userId").toLongOrNull()
|
||||||
val entityId = ctx.pathParam("entityId").toLongOrNull()
|
val entityName = ctx.pathParam("entityName")
|
||||||
val recordId = ctx.pathParam("recordId").toLongOrNull()
|
val recordId = ctx.pathParam("recordId").toLongOrNull()
|
||||||
if (userId == null || entityId == null || recordId == null || users.findById(userId) == null) {
|
if (userId == null || recordId == null || users.findById(userId) == null) {
|
||||||
ctx.notFound()
|
ctx.notFound()
|
||||||
return@patch
|
return@patch
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val record = records.updateRecordLinkedToUser(userId, entityId, recordId, ctx.singleFormParams())
|
val record = records.updateRecordLinkedToUser(userId, entityName, recordId, ctx.singleFormParams())
|
||||||
if (record == null) ctx.notFound() else ctx.json(record)
|
if (record == null) ctx.notFound() else ctx.json(record)
|
||||||
} catch (exception: IllegalArgumentException) {
|
} catch (exception: IllegalArgumentException) {
|
||||||
ctx.badRequest(exception.message ?: "Invalid field value")
|
ctx.badRequest(exception.message ?: "Invalid field value")
|
||||||
@ -69,16 +69,16 @@ class UserController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
routes.post(
|
routes.post(
|
||||||
"/api/users/{userId}/entities/{entityId}/records/{recordId}/children/{childEntityId}/{fieldId}",
|
"/api/users/{userId}/entities/{entityName}/records/{recordId}/children/{childEntityName}/{fieldId}",
|
||||||
) { ctx ->
|
) { ctx ->
|
||||||
if (!ctx.requireAdmin(loginService)) return@post
|
if (!ctx.requireAdmin(loginService)) return@post
|
||||||
val userId = ctx.pathParam("userId").toLongOrNull()
|
val userId = ctx.pathParam("userId").toLongOrNull()
|
||||||
val entityId = ctx.pathParam("entityId").toLongOrNull()
|
val entityName = ctx.pathParam("entityName")
|
||||||
val recordId = ctx.pathParam("recordId").toLongOrNull()
|
val recordId = ctx.pathParam("recordId").toLongOrNull()
|
||||||
val childEntityId = ctx.pathParam("childEntityId").toLongOrNull()
|
val childEntityName = ctx.pathParam("childEntityName")
|
||||||
val fieldId = ctx.pathParam("fieldId").toLongOrNull()
|
val fieldId = ctx.pathParam("fieldId").toLongOrNull()
|
||||||
if (
|
if (
|
||||||
userId == null || entityId == null || recordId == null || childEntityId == null || fieldId == null ||
|
userId == null || recordId == null || fieldId == null ||
|
||||||
users.findById(userId) == null
|
users.findById(userId) == null
|
||||||
) {
|
) {
|
||||||
ctx.notFound()
|
ctx.notFound()
|
||||||
@ -87,9 +87,9 @@ class UserController(
|
|||||||
try {
|
try {
|
||||||
val child = records.createChild(
|
val child = records.createChild(
|
||||||
userId,
|
userId,
|
||||||
entityId,
|
entityName,
|
||||||
recordId,
|
recordId,
|
||||||
childEntityId,
|
childEntityName,
|
||||||
fieldId,
|
fieldId,
|
||||||
ctx.singleFormParams(),
|
ctx.singleFormParams(),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS entity_definitions_name_case_insensitive_unique
|
||||||
|
ON entity_definitions (LOWER(name));
|
||||||
Loading…
x
Reference in New Issue
Block a user