add unique / index
This commit is contained in:
parent
1ec9f7e8e7
commit
285fc4aeec
@ -12,6 +12,8 @@
|
|||||||
type: FieldType
|
type: FieldType
|
||||||
targetEntityId?: number
|
targetEntityId?: number
|
||||||
relationshipType: RelationshipType
|
relationshipType: RelationshipType
|
||||||
|
indexed: boolean
|
||||||
|
unique: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EntityField {
|
interface EntityField {
|
||||||
@ -22,6 +24,8 @@
|
|||||||
targetEntityId?: number
|
targetEntityId?: number
|
||||||
targetEntityName?: string
|
targetEntityName?: string
|
||||||
relationshipType?: RelationshipType
|
relationshipType?: RelationshipType
|
||||||
|
indexed?: boolean
|
||||||
|
unique?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EntityDefinition {
|
interface EntityDefinition {
|
||||||
@ -61,6 +65,8 @@
|
|||||||
const editingFieldType = ref<FieldType>('TEXT')
|
const editingFieldType = ref<FieldType>('TEXT')
|
||||||
const editingTargetEntityId = ref<number>()
|
const editingTargetEntityId = ref<number>()
|
||||||
const editingRelationshipType = ref<RelationshipType>('ONE_TO_ONE')
|
const editingRelationshipType = ref<RelationshipType>('ONE_TO_ONE')
|
||||||
|
const editingFieldIndexed = ref(false)
|
||||||
|
const editingFieldUnique = ref(false)
|
||||||
const validatingMigration = ref(false)
|
const validatingMigration = ref(false)
|
||||||
const runningMigration = ref(false)
|
const runningMigration = ref(false)
|
||||||
const migrationValidation = ref<MigrationValidation>()
|
const migrationValidation = ref<MigrationValidation>()
|
||||||
@ -91,7 +97,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureNewField (entityId: number) {
|
function ensureNewField (entityId: number) {
|
||||||
newFields[entityId] ??= { name: '', identifier: '', type: 'TEXT', relationshipType: 'ONE_TO_ONE' }
|
newFields[entityId] ??= {
|
||||||
|
name: '',
|
||||||
|
identifier: '',
|
||||||
|
type: 'TEXT',
|
||||||
|
relationshipType: 'ONE_TO_ONE',
|
||||||
|
indexed: false,
|
||||||
|
unique: false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toIdentifier (name: string) {
|
function toIdentifier (name: string) {
|
||||||
@ -183,6 +196,8 @@
|
|||||||
name,
|
name,
|
||||||
identifier,
|
identifier,
|
||||||
type: draft.type,
|
type: draft.type,
|
||||||
|
indexed: String(draft.indexed),
|
||||||
|
unique: String(draft.unique),
|
||||||
}
|
}
|
||||||
if (draft.type === 'RELATIONSHIP') {
|
if (draft.type === 'RELATIONSHIP') {
|
||||||
if (!draft.targetEntityId) return
|
if (!draft.targetEntityId) return
|
||||||
@ -192,7 +207,14 @@
|
|||||||
const response = await request(`/api/entity-definitions/${entity.id}/fields`, 'POST', values)
|
const response = await request(`/api/entity-definitions/${entity.id}/fields`, 'POST', values)
|
||||||
if (!response.ok) throw new Error('Field create failed')
|
if (!response.ok) throw new Error('Field create failed')
|
||||||
entity.fields.push(await response.json() as EntityField)
|
entity.fields.push(await response.json() as EntityField)
|
||||||
newFields[entity.id] = { name: '', identifier: '', type: 'TEXT', relationshipType: 'ONE_TO_ONE' }
|
newFields[entity.id] = {
|
||||||
|
name: '',
|
||||||
|
identifier: '',
|
||||||
|
type: 'TEXT',
|
||||||
|
relationshipType: 'ONE_TO_ONE',
|
||||||
|
indexed: false,
|
||||||
|
unique: false,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,6 +225,8 @@
|
|||||||
editingFieldType.value = field.type
|
editingFieldType.value = field.type
|
||||||
editingTargetEntityId.value = field.targetEntityId
|
editingTargetEntityId.value = field.targetEntityId
|
||||||
editingRelationshipType.value = field.relationshipType ?? 'ONE_TO_ONE'
|
editingRelationshipType.value = field.relationshipType ?? 'ONE_TO_ONE'
|
||||||
|
editingFieldIndexed.value = field.indexed ?? false
|
||||||
|
editingFieldUnique.value = field.unique ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveField (entity: EntityDefinition, field: EntityField) {
|
async function saveField (entity: EntityDefinition, field: EntityField) {
|
||||||
@ -214,6 +238,8 @@
|
|||||||
name,
|
name,
|
||||||
identifier,
|
identifier,
|
||||||
type: editingFieldType.value,
|
type: editingFieldType.value,
|
||||||
|
indexed: String(editingFieldIndexed.value),
|
||||||
|
unique: String(editingFieldUnique.value),
|
||||||
}
|
}
|
||||||
if (editingFieldType.value === 'RELATIONSHIP') {
|
if (editingFieldType.value === 'RELATIONSHIP') {
|
||||||
if (!editingTargetEntityId.value) return
|
if (!editingTargetEntityId.value) return
|
||||||
@ -461,6 +487,16 @@
|
|||||||
</select>
|
</select>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input v-model="editingFieldIndexed" type="checkbox">
|
||||||
|
Indexed
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input v-model="editingFieldUnique" type="checkbox">
|
||||||
|
Unique
|
||||||
|
</label>
|
||||||
|
|
||||||
<button class="text-button" type="submit">Save field</button>
|
<button class="text-button" type="submit">Save field</button>
|
||||||
<button class="text-button muted" type="button" @click="editingFieldId = undefined">Cancel</button>
|
<button class="text-button muted" type="button" @click="editingFieldId = undefined">Cancel</button>
|
||||||
</form>
|
</form>
|
||||||
@ -470,6 +506,8 @@
|
|||||||
<strong>{{ field.name }}</strong>
|
<strong>{{ field.name }}</strong>
|
||||||
<span class="identifier">{{ field.identifier }}</span>
|
<span class="identifier">{{ field.identifier }}</span>
|
||||||
<span class="type-badge">{{ typeLabel(field.type) }}</span>
|
<span class="type-badge">{{ typeLabel(field.type) }}</span>
|
||||||
|
<span v-if="field.indexed && !field.unique" class="option-badge">Indexed</span>
|
||||||
|
<span v-if="field.unique" class="option-badge">Unique</span>
|
||||||
|
|
||||||
<span v-if="field.type === 'RELATIONSHIP'" class="reference-detail">
|
<span v-if="field.type === 'RELATIONSHIP'" class="reference-detail">
|
||||||
{{ targetName(field) }} · {{ relationshipLabel(field.relationshipType) }}
|
{{ targetName(field) }} · {{ relationshipLabel(field.relationshipType) }}
|
||||||
@ -522,6 +560,16 @@
|
|||||||
</select>
|
</select>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input v-model="newFields[entity.id].indexed" type="checkbox">
|
||||||
|
Indexed
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input v-model="newFields[entity.id].unique" type="checkbox">
|
||||||
|
Unique
|
||||||
|
</label>
|
||||||
|
|
||||||
<button class="secondary-button" :disabled="saving" type="submit">Add field</button>
|
<button class="secondary-button" :disabled="saving" type="submit">Add field</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@ -559,6 +607,9 @@
|
|||||||
.field-row:last-child { border-bottom: 0; }
|
.field-row:last-child { border-bottom: 0; }
|
||||||
.field-row strong { margin-right: 0.75rem; }
|
.field-row strong { margin-right: 0.75rem; }
|
||||||
.type-badge { padding: 0.2rem 0.55rem; border-radius: 999px; color: var(--v0-primary); background: color-mix(in srgb, var(--v0-primary) 13%, transparent); font-size: 0.7rem; font-weight: 700; }
|
.type-badge { padding: 0.2rem 0.55rem; border-radius: 999px; color: var(--v0-primary); background: color-mix(in srgb, var(--v0-primary) 13%, transparent); font-size: 0.7rem; font-weight: 700; }
|
||||||
|
.option-badge { margin-left: 0.4rem; padding: 0.2rem 0.55rem; border-radius: 999px; color: var(--v0-on-surface-variant); background: var(--v0-surface-variant); font-size: 0.7rem; font-weight: 700; }
|
||||||
|
.checkbox-label { display: inline-flex; margin: 0; gap: 0.35rem; align-items: center; white-space: nowrap; font-weight: 600; }
|
||||||
|
.checkbox-label input { width: 1rem; height: 1rem; padding: 0; accent-color: var(--v0-primary); }
|
||||||
.reference-detail { margin-left: 0.65rem; color: var(--v0-on-surface-variant); font-size: 0.8rem; }
|
.reference-detail { margin-left: 0.65rem; color: var(--v0-on-surface-variant); font-size: 0.8rem; }
|
||||||
.empty-fields { color: var(--v0-on-surface-variant); font-size: 0.875rem; }
|
.empty-fields { color: var(--v0-on-surface-variant); font-size: 0.875rem; }
|
||||||
.add-field { padding: 1rem 1.25rem 1.25rem; border-top: 1px solid var(--v0-divider); background: color-mix(in srgb, var(--v0-surface-variant) 35%, transparent); }
|
.add-field { padding: 1rem 1.25rem 1.25rem; border-top: 1px solid var(--v0-divider); background: color-mix(in srgb, var(--v0-surface-variant) 35%, transparent); }
|
||||||
@ -581,7 +632,7 @@
|
|||||||
.entity-heading, .field-row { gap: 0.75rem; align-items: flex-start; flex-direction: column; }
|
.entity-heading, .field-row { gap: 0.75rem; align-items: flex-start; flex-direction: column; }
|
||||||
.form-row, .field-inputs, .edit-row, .edit-field { align-items: stretch; flex-direction: column; }
|
.form-row, .field-inputs, .edit-row, .edit-field { align-items: stretch; flex-direction: column; }
|
||||||
.entity-heading, .field-row { padding-block: 1rem; }
|
.entity-heading, .field-row { padding-block: 1rem; }
|
||||||
input, select, .primary-button, .secondary-button { width: 100%; }
|
input:not([type="checkbox"]), select, .primary-button, .secondary-button { width: 100%; }
|
||||||
.migration-actions { align-items: stretch; flex-direction: column; }
|
.migration-actions { align-items: stretch; flex-direction: column; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -7,6 +7,8 @@ interface EntityField {
|
|||||||
type: string
|
type: string
|
||||||
targetEntityId?: number
|
targetEntityId?: number
|
||||||
relationshipType?: string
|
relationshipType?: string
|
||||||
|
indexed?: boolean
|
||||||
|
unique?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EntityDefinition {
|
interface EntityDefinition {
|
||||||
@ -80,6 +82,8 @@ test('admin can add, edit, and remove an entity and its fields', async ({ page }
|
|||||||
name: values.name,
|
name: values.name,
|
||||||
identifier: values.identifier,
|
identifier: values.identifier,
|
||||||
type: values.type,
|
type: values.type,
|
||||||
|
indexed: values.indexed === 'true',
|
||||||
|
unique: values.unique === 'true',
|
||||||
...(values.targetEntityId ? { targetEntityId: Number(values.targetEntityId) } : {}),
|
...(values.targetEntityId ? { targetEntityId: Number(values.targetEntityId) } : {}),
|
||||||
...(values.relationshipType ? { relationshipType: values.relationshipType } : {}),
|
...(values.relationshipType ? { relationshipType: values.relationshipType } : {}),
|
||||||
}
|
}
|
||||||
@ -121,9 +125,12 @@ test('admin can add, edit, and remove an entity and its fields', async ({ page }
|
|||||||
await expect(page.getByLabel('Field identifier')).toHaveValue('website')
|
await expect(page.getByLabel('Field identifier')).toHaveValue('website')
|
||||||
await page.getByLabel('Field identifier').fill('websiteUrl')
|
await page.getByLabel('Field identifier').fill('websiteUrl')
|
||||||
await page.getByLabel('Field type').selectOption('EMAIL')
|
await page.getByLabel('Field type').selectOption('EMAIL')
|
||||||
|
await page.getByRole('form', { name: 'Add field to Organization' }).getByLabel('Indexed').check()
|
||||||
|
await page.getByRole('form', { name: 'Add field to Organization' }).getByLabel('Unique').check()
|
||||||
await page.getByRole('button', { name: 'Add field' }).click()
|
await page.getByRole('button', { name: 'Add field' }).click()
|
||||||
await expect(page.getByText('Website', { exact: true })).toBeVisible()
|
await expect(page.getByText('Website', { exact: true })).toBeVisible()
|
||||||
await expect(page.locator('.type-badge')).toHaveText('Email')
|
await expect(page.locator('.type-badge')).toHaveText('Email')
|
||||||
|
await expect(page.locator('.option-badge', { hasText: 'Unique' })).toBeVisible()
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Edit field' }).click()
|
await page.getByRole('button', { name: 'Edit field' }).click()
|
||||||
const fieldEditForm = page.getByRole('button', { name: 'Save field' }).locator('..')
|
const fieldEditForm = page.getByRole('button', { name: 'Save field' }).locator('..')
|
||||||
|
|||||||
@ -8,6 +8,8 @@ data class EntityField(
|
|||||||
val targetEntityId: Long? = null,
|
val targetEntityId: Long? = null,
|
||||||
val targetEntityName: String? = null,
|
val targetEntityName: String? = null,
|
||||||
val relationshipType: RelationshipType? = null,
|
val relationshipType: RelationshipType? = null,
|
||||||
|
val indexed: Boolean = false,
|
||||||
|
val unique: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class EntityDefinition(
|
data class EntityDefinition(
|
||||||
|
|||||||
@ -9,7 +9,8 @@ class EntityDefinitionRepository(private val database: Database) {
|
|||||||
"""
|
"""
|
||||||
SELECT entity_fields.id, entity_fields.entity_id, entity_fields.name, entity_fields.identifier,
|
SELECT entity_fields.id, entity_fields.entity_id, entity_fields.name, entity_fields.identifier,
|
||||||
entity_fields.field_type, entity_fields.target_entity_id,
|
entity_fields.field_type, entity_fields.target_entity_id,
|
||||||
entity_fields.relationship_type, targets.name AS target_entity_name
|
entity_fields.relationship_type, entity_fields.indexed, entity_fields.is_unique,
|
||||||
|
targets.name AS target_entity_name
|
||||||
FROM entity_fields
|
FROM entity_fields
|
||||||
LEFT JOIN entity_definitions targets ON targets.id = entity_fields.target_entity_id
|
LEFT JOIN entity_definitions targets ON targets.id = entity_fields.target_entity_id
|
||||||
ORDER BY entity_fields.id
|
ORDER BY entity_fields.id
|
||||||
@ -59,11 +60,15 @@ class EntityDefinitionRepository(private val database: Database) {
|
|||||||
type: FieldType,
|
type: FieldType,
|
||||||
targetEntityId: Long?,
|
targetEntityId: Long?,
|
||||||
relationshipType: RelationshipType?,
|
relationshipType: RelationshipType?,
|
||||||
|
indexed: Boolean,
|
||||||
|
unique: Boolean,
|
||||||
): EntityField? {
|
): EntityField? {
|
||||||
val sql = """
|
val sql = """
|
||||||
INSERT INTO entity_fields (entity_id, name, identifier, field_type, target_entity_id, relationship_type)
|
INSERT INTO entity_fields (
|
||||||
SELECT id, ?, ?, ?, ?, ? FROM entity_definitions WHERE id = ?
|
entity_id, name, identifier, field_type, target_entity_id, relationship_type, indexed, is_unique
|
||||||
RETURNING id, name, identifier, field_type, target_entity_id, relationship_type
|
)
|
||||||
|
SELECT id, ?, ?, ?, ?, ?, ?, ? FROM entity_definitions WHERE id = ?
|
||||||
|
RETURNING id, name, identifier, field_type, target_entity_id, relationship_type, indexed, is_unique
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
return database.queryOne(sql, bind = {
|
return database.queryOne(sql, bind = {
|
||||||
setString(1, name)
|
setString(1, name)
|
||||||
@ -71,7 +76,9 @@ class EntityDefinitionRepository(private val database: Database) {
|
|||||||
setString(3, type.name)
|
setString(3, type.name)
|
||||||
setObject(4, targetEntityId)
|
setObject(4, targetEntityId)
|
||||||
setString(5, relationshipType?.name)
|
setString(5, relationshipType?.name)
|
||||||
setLong(6, entityId)
|
setBoolean(6, indexed)
|
||||||
|
setBoolean(7, unique)
|
||||||
|
setLong(8, entityId)
|
||||||
}) { toEntityField() }
|
}) { toEntityField() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,12 +90,15 @@ class EntityDefinitionRepository(private val database: Database) {
|
|||||||
type: FieldType,
|
type: FieldType,
|
||||||
targetEntityId: Long?,
|
targetEntityId: Long?,
|
||||||
relationshipType: RelationshipType?,
|
relationshipType: RelationshipType?,
|
||||||
|
indexed: Boolean,
|
||||||
|
unique: Boolean,
|
||||||
): EntityField? {
|
): EntityField? {
|
||||||
val sql = """
|
val sql = """
|
||||||
UPDATE entity_fields
|
UPDATE entity_fields
|
||||||
SET name = ?, identifier = ?, field_type = ?, target_entity_id = ?, relationship_type = ?
|
SET name = ?, identifier = ?, field_type = ?, target_entity_id = ?, relationship_type = ?,
|
||||||
|
indexed = ?, is_unique = ?
|
||||||
WHERE id = ? AND entity_id = ?
|
WHERE id = ? AND entity_id = ?
|
||||||
RETURNING id, name, identifier, field_type, target_entity_id, relationship_type
|
RETURNING id, name, identifier, field_type, target_entity_id, relationship_type, indexed, is_unique
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
return database.queryOne(sql, bind = {
|
return database.queryOne(sql, bind = {
|
||||||
setString(1, name)
|
setString(1, name)
|
||||||
@ -96,8 +106,10 @@ class EntityDefinitionRepository(private val database: Database) {
|
|||||||
setString(3, type.name)
|
setString(3, type.name)
|
||||||
setObject(4, targetEntityId)
|
setObject(4, targetEntityId)
|
||||||
setString(5, relationshipType?.name)
|
setString(5, relationshipType?.name)
|
||||||
setLong(6, fieldId)
|
setBoolean(6, indexed)
|
||||||
setLong(7, entityId)
|
setBoolean(7, unique)
|
||||||
|
setLong(8, fieldId)
|
||||||
|
setLong(9, entityId)
|
||||||
}) { toEntityField() }
|
}) { toEntityField() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,6 +124,7 @@ class EntityDefinitionRepository(private val database: Database) {
|
|||||||
"""
|
"""
|
||||||
SELECT entity_fields.id, entity_fields.name, entity_fields.identifier, entity_fields.field_type,
|
SELECT entity_fields.id, entity_fields.name, entity_fields.identifier, entity_fields.field_type,
|
||||||
entity_fields.target_entity_id, entity_fields.relationship_type,
|
entity_fields.target_entity_id, entity_fields.relationship_type,
|
||||||
|
entity_fields.indexed, entity_fields.is_unique,
|
||||||
targets.name AS target_entity_name
|
targets.name AS target_entity_name
|
||||||
FROM entity_fields
|
FROM entity_fields
|
||||||
LEFT JOIN entity_definitions targets ON targets.id = entity_fields.target_entity_id
|
LEFT JOIN entity_definitions targets ON targets.id = entity_fields.target_entity_id
|
||||||
@ -132,4 +145,6 @@ private fun ResultSet.toEntityField() = EntityField(
|
|||||||
targetEntityId = getLong("target_entity_id").takeUnless { wasNull() },
|
targetEntityId = getLong("target_entity_id").takeUnless { wasNull() },
|
||||||
targetEntityName = runCatching { getString("target_entity_name") }.getOrNull(),
|
targetEntityName = runCatching { getString("target_entity_name") }.getOrNull(),
|
||||||
relationshipType = getString("relationship_type")?.let(RelationshipType::valueOf),
|
relationshipType = getString("relationship_type")?.let(RelationshipType::valueOf),
|
||||||
|
indexed = getBoolean("indexed"),
|
||||||
|
unique = getBoolean("is_unique"),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -156,15 +156,26 @@ class EntityMigrationService(
|
|||||||
"Add reference ${entity.identifier}.${field.identifier} → $target.id.",
|
"Add reference ${entity.identifier}.${field.identifier} → $target.id.",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (field.relationshipType == RelationshipType.ONE_TO_ONE) {
|
}
|
||||||
val unique = "em_uq_${entity.id}_${field.id}"
|
}
|
||||||
if (!constraintExists(connection, unique)) {
|
|
||||||
statements += MigrationStatement(
|
definitions.forEach { entity ->
|
||||||
"ALTER TABLE ${quote(table)} ADD CONSTRAINT ${quote(unique)} " +
|
val table = existingTables[entity.identifier.lowercase()] ?: entity.identifier
|
||||||
"UNIQUE (${quote(field.identifier)})",
|
entity.fields.forEach { field ->
|
||||||
"Make ${entity.identifier}.${field.identifier} one-to-one.",
|
val requiresUnique = field.unique || field.relationshipType == RelationshipType.ONE_TO_ONE
|
||||||
)
|
val uniqueName = "em_uq_${entity.id}_${field.id}"
|
||||||
}
|
val indexName = "em_idx_${entity.id}_${field.id}"
|
||||||
|
if (requiresUnique && !constraintExists(connection, uniqueName)) {
|
||||||
|
statements += MigrationStatement(
|
||||||
|
"ALTER TABLE ${quote(table)} ADD CONSTRAINT ${quote(uniqueName)} " +
|
||||||
|
"UNIQUE (${quote(field.identifier)})",
|
||||||
|
"Add a unique constraint to ${entity.identifier}.${field.identifier}.",
|
||||||
|
)
|
||||||
|
} else if (field.indexed && !requiresUnique && !indexExists(connection, indexName)) {
|
||||||
|
statements += MigrationStatement(
|
||||||
|
"CREATE INDEX ${quote(indexName)} ON ${quote(table)} (${quote(field.identifier)})",
|
||||||
|
"Index ${entity.identifier}.${field.identifier}.",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -176,7 +187,8 @@ class EntityMigrationService(
|
|||||||
val fields = mutableMapOf<Long, MutableList<EntityField>>()
|
val fields = mutableMapOf<Long, MutableList<EntityField>>()
|
||||||
connection.prepareStatement(
|
connection.prepareStatement(
|
||||||
"""
|
"""
|
||||||
SELECT id, entity_id, name, identifier, field_type, target_entity_id, relationship_type
|
SELECT id, entity_id, name, identifier, field_type, target_entity_id, relationship_type,
|
||||||
|
indexed, is_unique
|
||||||
FROM entity_fields ORDER BY id
|
FROM entity_fields ORDER BY id
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
).use { statement ->
|
).use { statement ->
|
||||||
@ -190,6 +202,8 @@ class EntityMigrationService(
|
|||||||
type = FieldType.valueOf(result.getString("field_type")),
|
type = FieldType.valueOf(result.getString("field_type")),
|
||||||
targetEntityId = result.getLong("target_entity_id").takeUnless { result.wasNull() },
|
targetEntityId = result.getLong("target_entity_id").takeUnless { result.wasNull() },
|
||||||
relationshipType = result.getString("relationship_type")?.let(RelationshipType::valueOf),
|
relationshipType = result.getString("relationship_type")?.let(RelationshipType::valueOf),
|
||||||
|
indexed = result.getBoolean("indexed"),
|
||||||
|
unique = result.getBoolean("is_unique"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -257,6 +271,14 @@ class EntityMigrationService(
|
|||||||
statement.executeQuery().use { it.next() }
|
statement.executeQuery().use { it.next() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun indexExists(connection: Connection, name: String): Boolean =
|
||||||
|
connection.prepareStatement(
|
||||||
|
"SELECT 1 FROM pg_indexes WHERE schemaname = current_schema() AND indexname = ?",
|
||||||
|
).use { statement ->
|
||||||
|
statement.setString(1, name)
|
||||||
|
statement.executeQuery().use { it.next() }
|
||||||
|
}
|
||||||
|
|
||||||
private fun sqlType(type: FieldType): String = when (type) {
|
private fun sqlType(type: FieldType): String = when (type) {
|
||||||
FieldType.TEXT -> "TEXT"
|
FieldType.TEXT -> "TEXT"
|
||||||
FieldType.NUMBER -> "NUMERIC"
|
FieldType.NUMBER -> "NUMERIC"
|
||||||
|
|||||||
@ -67,6 +67,8 @@ class EntityDefinitionController(
|
|||||||
type,
|
type,
|
||||||
relationship.targetEntityId,
|
relationship.targetEntityId,
|
||||||
relationship.type,
|
relationship.type,
|
||||||
|
ctx.booleanFormParam("indexed"),
|
||||||
|
ctx.booleanFormParam("unique"),
|
||||||
)
|
)
|
||||||
if (field == null) ctx.notFound() else {
|
if (field == null) ctx.notFound() else {
|
||||||
ctx.status(HttpStatus.CREATED).json(field)
|
ctx.status(HttpStatus.CREATED).json(field)
|
||||||
@ -95,6 +97,8 @@ class EntityDefinitionController(
|
|||||||
type,
|
type,
|
||||||
relationship.targetEntityId,
|
relationship.targetEntityId,
|
||||||
relationship.type,
|
relationship.type,
|
||||||
|
ctx.booleanFormParam("indexed"),
|
||||||
|
ctx.booleanFormParam("unique"),
|
||||||
)
|
)
|
||||||
if (field == null) ctx.notFound() else ctx.json(field)
|
if (field == null) ctx.notFound() else ctx.json(field)
|
||||||
}
|
}
|
||||||
@ -130,4 +134,7 @@ class EntityDefinitionController(
|
|||||||
val targetEntityId: Long?,
|
val targetEntityId: Long?,
|
||||||
val type: RelationshipType?,
|
val type: RelationshipType?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun Context.booleanFormParam(name: String): Boolean =
|
||||||
|
formParam(name)?.equals("true", ignoreCase = true) == true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE entity_fields
|
||||||
|
ADD COLUMN IF NOT EXISTS indexed BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
|
|
||||||
|
ALTER TABLE entity_fields
|
||||||
|
ADD COLUMN IF NOT EXISTS is_unique BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
@ -42,7 +42,7 @@ class ResponseSerializationTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"""{"id":1,"name":"Company","identifier":"company","fields":[{"id":2,"name":"Annual revenue","identifier":"annualRevenue","type":"NUMBER","targetEntityId":null,"targetEntityName":null,"relationshipType":null}]}""",
|
"""{"id":1,"name":"Company","identifier":"company","fields":[{"id":2,"name":"Annual revenue","identifier":"annualRevenue","type":"NUMBER","targetEntityId":null,"targetEntityName":null,"relationshipType":null,"indexed":false,"unique":false}]}""",
|
||||||
mapper.writeValueAsString(entity),
|
mapper.writeValueAsString(entity),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user