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 userFilePath = computed(() => `/users/${route.params.userId}`)
|
||||
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) {
|
||||
@ -146,7 +147,7 @@
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
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',
|
||||
headers: {
|
||||
@ -305,7 +306,7 @@
|
||||
<router-link
|
||||
:aria-label="`View ${child.entity.name} record ${childRecord.id}`"
|
||||
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
|
||||
</router-link>
|
||||
|
||||
@ -103,7 +103,7 @@
|
||||
<router-link
|
||||
:aria-label="`View ${linked.entity.name} record ${record.id}`"
|
||||
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
|
||||
</router-link>
|
||||
|
||||
@ -31,7 +31,7 @@ const router = createRouter({
|
||||
meta: { requiresAdmin: true },
|
||||
},
|
||||
{
|
||||
path: '/users/:userId/entities/:entityId/records/:recordId',
|
||||
path: '/users/:userId/entities/:entityName/records/:recordId',
|
||||
component: EntityRecordViewer,
|
||||
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') {
|
||||
const values = new URLSearchParams(route.request().postData() ?? '')
|
||||
projectTitle = values.get('title') ?? projectTitle
|
||||
}
|
||||
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 created = { id: 55, description: values.get('description'), project: 42 }
|
||||
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 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.getByText('Website refresh')).toBeVisible()
|
||||
await expect(page.getByText('Owner')).toBeVisible()
|
||||
|
||||
@ -4,6 +4,21 @@ import dev.mduchene.bolts.persistence.Database
|
||||
import java.sql.ResultSet
|
||||
|
||||
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> {
|
||||
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 entity = definitions.firstOrNull { it.id == entityId } ?: return null
|
||||
val entity = definitions.firstOrNull { it.name == entityName } ?: return null
|
||||
val values = findValues(entity, recordId) ?: return null
|
||||
if (!isAccessibleToUser(entity, values, userId, definitions, mutableSetOf())) return null
|
||||
return EntityRecord(entity, values, findChildren(entity.id, recordId))
|
||||
@ -61,11 +61,11 @@ class EntityRecordRepository(
|
||||
|
||||
fun updateRecordLinkedToUser(
|
||||
userId: Long,
|
||||
entityId: Long,
|
||||
entityName: String,
|
||||
recordId: Long,
|
||||
submittedValues: Map<String, String>,
|
||||
): 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 }
|
||||
if (fields.isNotEmpty()) {
|
||||
val assignments = fields.joinToString { "${quote(it.identifier)} = ?" }
|
||||
@ -77,21 +77,23 @@ class EntityRecordRepository(
|
||||
setLong(fields.size + 1, recordId)
|
||||
}
|
||||
}
|
||||
return findRecordLinkedToUser(userId, entityId, recordId)
|
||||
return findRecordLinkedToUser(userId, entityName, recordId)
|
||||
}
|
||||
|
||||
fun createChild(
|
||||
userId: Long,
|
||||
parentEntityId: Long,
|
||||
parentEntityName: String,
|
||||
parentRecordId: Long,
|
||||
childEntityId: Long,
|
||||
childEntityName: String,
|
||||
relationshipFieldId: Long,
|
||||
submittedValues: Map<String, String>,
|
||||
): EntityRecord? {
|
||||
if (findRecordLinkedToUser(userId, parentEntityId, parentRecordId) == null) return null
|
||||
val child = entities.findAll().firstOrNull { it.id == childEntityId } ?: return null
|
||||
val definitions = entities.findAll()
|
||||
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 {
|
||||
it.id == relationshipFieldId && it.type == FieldType.RELATIONSHIP && it.targetEntityId == parentEntityId
|
||||
it.id == relationshipFieldId && it.type == FieldType.RELATIONSHIP && it.targetEntityId == parent.id
|
||||
} ?: return null
|
||||
val fields = child.fields
|
||||
val sql = "INSERT INTO ${quote(child.identifier)} (${fields.joinToString { quote(it.identifier) }}) " +
|
||||
|
||||
@ -33,6 +33,10 @@ class EntityDefinitionController(
|
||||
if (!ctx.requireAdmin(loginService)) return@post
|
||||
val name = ctx.requiredFormParam("name") ?: 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))
|
||||
}
|
||||
routes.patch("/api/entity-definitions/{entityId}") { ctx ->
|
||||
@ -41,6 +45,10 @@ class EntityDefinitionController(
|
||||
val name = ctx.requiredFormParam("name")
|
||||
val identifier = ctx.requiredFormParam("identifier")
|
||||
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)
|
||||
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
|
||||
val userId = ctx.pathParam("userId").toLongOrNull()
|
||||
val entityId = ctx.pathParam("entityId").toLongOrNull()
|
||||
val entityName = ctx.pathParam("entityName")
|
||||
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()
|
||||
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)
|
||||
}
|
||||
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
|
||||
val userId = ctx.pathParam("userId").toLongOrNull()
|
||||
val entityId = ctx.pathParam("entityId").toLongOrNull()
|
||||
val entityName = ctx.pathParam("entityName")
|
||||
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()
|
||||
return@patch
|
||||
}
|
||||
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)
|
||||
} catch (exception: IllegalArgumentException) {
|
||||
ctx.badRequest(exception.message ?: "Invalid field value")
|
||||
@ -69,16 +69,16 @@ class UserController(
|
||||
}
|
||||
}
|
||||
routes.post(
|
||||
"/api/users/{userId}/entities/{entityId}/records/{recordId}/children/{childEntityId}/{fieldId}",
|
||||
"/api/users/{userId}/entities/{entityName}/records/{recordId}/children/{childEntityName}/{fieldId}",
|
||||
) { ctx ->
|
||||
if (!ctx.requireAdmin(loginService)) return@post
|
||||
val userId = ctx.pathParam("userId").toLongOrNull()
|
||||
val entityId = ctx.pathParam("entityId").toLongOrNull()
|
||||
val entityName = ctx.pathParam("entityName")
|
||||
val recordId = ctx.pathParam("recordId").toLongOrNull()
|
||||
val childEntityId = ctx.pathParam("childEntityId").toLongOrNull()
|
||||
val childEntityName = ctx.pathParam("childEntityName")
|
||||
val fieldId = ctx.pathParam("fieldId").toLongOrNull()
|
||||
if (
|
||||
userId == null || entityId == null || recordId == null || childEntityId == null || fieldId == null ||
|
||||
userId == null || recordId == null || fieldId == null ||
|
||||
users.findById(userId) == null
|
||||
) {
|
||||
ctx.notFound()
|
||||
@ -87,9 +87,9 @@ class UserController(
|
||||
try {
|
||||
val child = records.createChild(
|
||||
userId,
|
||||
entityId,
|
||||
entityName,
|
||||
recordId,
|
||||
childEntityId,
|
||||
childEntityName,
|
||||
fieldId,
|
||||
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