update
This commit is contained in:
+20
@@ -14,6 +14,13 @@ export type ScannedRoutes = {
|
|||||||
'auth.login': { paramsTuple?: []; params?: {} }
|
'auth.login': { paramsTuple?: []; params?: {} }
|
||||||
'auth.logout': { paramsTuple?: []; params?: {} }
|
'auth.logout': { paramsTuple?: []; params?: {} }
|
||||||
'auth.user': { paramsTuple?: []; params?: {} }
|
'auth.user': { paramsTuple?: []; params?: {} }
|
||||||
|
'contacts.index': { paramsTuple?: []; params?: {} }
|
||||||
|
'contacts.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
||||||
|
'contacts.store': { paramsTuple?: []; params?: {} }
|
||||||
|
'contacts.update': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
||||||
|
'contacts.destroy': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
||||||
|
'custom_fields.index': { paramsTuple?: []; params?: {} }
|
||||||
|
'custom_fields.store': { paramsTuple?: []; params?: {} }
|
||||||
'root': { paramsTuple?: []; params?: {} }
|
'root': { paramsTuple?: []; params?: {} }
|
||||||
'file_manager.rename': { paramsTuple?: []; params?: {} }
|
'file_manager.rename': { paramsTuple?: []; params?: {} }
|
||||||
'file_manager.create_folder': { paramsTuple?: []; params?: {} }
|
'file_manager.create_folder': { paramsTuple?: []; params?: {} }
|
||||||
@@ -34,6 +41,7 @@ export type ScannedRoutes = {
|
|||||||
'role.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
'role.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
||||||
'role.update': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
'role.update': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
||||||
'role.destroy': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
'role.destroy': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
||||||
|
'tenant.current': { paramsTuple?: []; params?: {} }
|
||||||
'public_web_manager.blog_list': { paramsTuple?: []; params?: {} }
|
'public_web_manager.blog_list': { paramsTuple?: []; params?: {} }
|
||||||
'public_web_manager.sitemap': { paramsTuple?: []; params?: {} }
|
'public_web_manager.sitemap': { paramsTuple?: []; params?: {} }
|
||||||
'public_web_manager.resolve': { paramsTuple?: []; params?: {} }
|
'public_web_manager.resolve': { paramsTuple?: []; params?: {} }
|
||||||
@@ -59,6 +67,9 @@ export type ScannedRoutes = {
|
|||||||
'admin.index': { paramsTuple?: []; params?: {} }
|
'admin.index': { paramsTuple?: []; params?: {} }
|
||||||
'admin.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
'admin.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
||||||
'auth.user': { paramsTuple?: []; params?: {} }
|
'auth.user': { paramsTuple?: []; params?: {} }
|
||||||
|
'contacts.index': { paramsTuple?: []; params?: {} }
|
||||||
|
'contacts.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
||||||
|
'custom_fields.index': { paramsTuple?: []; params?: {} }
|
||||||
'root': { paramsTuple?: []; params?: {} }
|
'root': { paramsTuple?: []; params?: {} }
|
||||||
'file_manager.raw': { paramsTuple: [...ParamValue[]]; params: {'*': ParamValue[]} }
|
'file_manager.raw': { paramsTuple: [...ParamValue[]]; params: {'*': ParamValue[]} }
|
||||||
'file_manager.show': { paramsTuple: [...ParamValue[]]; params: {'*': ParamValue[]} }
|
'file_manager.show': { paramsTuple: [...ParamValue[]]; params: {'*': ParamValue[]} }
|
||||||
@@ -68,6 +79,7 @@ export type ScannedRoutes = {
|
|||||||
'permission.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
'permission.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
||||||
'role.index': { paramsTuple?: []; params?: {} }
|
'role.index': { paramsTuple?: []; params?: {} }
|
||||||
'role.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
'role.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
||||||
|
'tenant.current': { paramsTuple?: []; params?: {} }
|
||||||
'public_web_manager.blog_list': { paramsTuple?: []; params?: {} }
|
'public_web_manager.blog_list': { paramsTuple?: []; params?: {} }
|
||||||
'public_web_manager.sitemap': { paramsTuple?: []; params?: {} }
|
'public_web_manager.sitemap': { paramsTuple?: []; params?: {} }
|
||||||
'public_web_manager.resolve': { paramsTuple?: []; params?: {} }
|
'public_web_manager.resolve': { paramsTuple?: []; params?: {} }
|
||||||
@@ -85,6 +97,9 @@ export type ScannedRoutes = {
|
|||||||
'admin.index': { paramsTuple?: []; params?: {} }
|
'admin.index': { paramsTuple?: []; params?: {} }
|
||||||
'admin.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
'admin.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
||||||
'auth.user': { paramsTuple?: []; params?: {} }
|
'auth.user': { paramsTuple?: []; params?: {} }
|
||||||
|
'contacts.index': { paramsTuple?: []; params?: {} }
|
||||||
|
'contacts.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
||||||
|
'custom_fields.index': { paramsTuple?: []; params?: {} }
|
||||||
'root': { paramsTuple?: []; params?: {} }
|
'root': { paramsTuple?: []; params?: {} }
|
||||||
'file_manager.raw': { paramsTuple: [...ParamValue[]]; params: {'*': ParamValue[]} }
|
'file_manager.raw': { paramsTuple: [...ParamValue[]]; params: {'*': ParamValue[]} }
|
||||||
'file_manager.show': { paramsTuple: [...ParamValue[]]; params: {'*': ParamValue[]} }
|
'file_manager.show': { paramsTuple: [...ParamValue[]]; params: {'*': ParamValue[]} }
|
||||||
@@ -94,6 +109,7 @@ export type ScannedRoutes = {
|
|||||||
'permission.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
'permission.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
||||||
'role.index': { paramsTuple?: []; params?: {} }
|
'role.index': { paramsTuple?: []; params?: {} }
|
||||||
'role.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
'role.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
||||||
|
'tenant.current': { paramsTuple?: []; params?: {} }
|
||||||
'public_web_manager.blog_list': { paramsTuple?: []; params?: {} }
|
'public_web_manager.blog_list': { paramsTuple?: []; params?: {} }
|
||||||
'public_web_manager.sitemap': { paramsTuple?: []; params?: {} }
|
'public_web_manager.sitemap': { paramsTuple?: []; params?: {} }
|
||||||
'public_web_manager.resolve': { paramsTuple?: []; params?: {} }
|
'public_web_manager.resolve': { paramsTuple?: []; params?: {} }
|
||||||
@@ -109,6 +125,7 @@ export type ScannedRoutes = {
|
|||||||
PUT: {
|
PUT: {
|
||||||
'admin.update_profile': { paramsTuple?: []; params?: {} }
|
'admin.update_profile': { paramsTuple?: []; params?: {} }
|
||||||
'admin.update': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
'admin.update': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
||||||
|
'contacts.update': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
||||||
'file_manager.rename': { paramsTuple?: []; params?: {} }
|
'file_manager.rename': { paramsTuple?: []; params?: {} }
|
||||||
'permission.update': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
'permission.update': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
||||||
'role.update': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
'role.update': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
||||||
@@ -119,6 +136,8 @@ export type ScannedRoutes = {
|
|||||||
'admin.store': { paramsTuple?: []; params?: {} }
|
'admin.store': { paramsTuple?: []; params?: {} }
|
||||||
'auth.login': { paramsTuple?: []; params?: {} }
|
'auth.login': { paramsTuple?: []; params?: {} }
|
||||||
'auth.logout': { paramsTuple?: []; params?: {} }
|
'auth.logout': { paramsTuple?: []; params?: {} }
|
||||||
|
'contacts.store': { paramsTuple?: []; params?: {} }
|
||||||
|
'custom_fields.store': { paramsTuple?: []; params?: {} }
|
||||||
'file_manager.create_folder': { paramsTuple?: []; params?: {} }
|
'file_manager.create_folder': { paramsTuple?: []; params?: {} }
|
||||||
'file_manager.upload': { paramsTuple?: []; params?: {} }
|
'file_manager.upload': { paramsTuple?: []; params?: {} }
|
||||||
'file_manager.duplicate': { paramsTuple?: []; params?: {} }
|
'file_manager.duplicate': { paramsTuple?: []; params?: {} }
|
||||||
@@ -137,6 +156,7 @@ export type ScannedRoutes = {
|
|||||||
}
|
}
|
||||||
DELETE: {
|
DELETE: {
|
||||||
'admin.destroy': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
'admin.destroy': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
||||||
|
'contacts.destroy': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
||||||
'file_manager.destroy': { paramsTuple: [...ParamValue[]]; params: {'*': ParamValue[]} }
|
'file_manager.destroy': { paramsTuple: [...ParamValue[]]; params: {'*': ParamValue[]} }
|
||||||
'permission.destroy': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
'permission.destroy': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
||||||
'role.destroy': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
'role.destroy': { paramsTuple: [ParamValue]; params: {'id': ParamValue} }
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { HttpContext } from '@adonisjs/core/http'
|
import type { HttpContext } from '@adonisjs/core/http'
|
||||||
import CustomField from '#base_modules/CustomField/model/custom_field'
|
import CustomField from '#base_modules/CustomField/model/custom_field'
|
||||||
|
import { createCustomFieldValidator } from '#base_modules/CustomField/validators/custom_field_create_validator'
|
||||||
|
|
||||||
export default class CustomFieldsController {
|
export default class CustomFieldsController {
|
||||||
/**
|
/**
|
||||||
@@ -13,4 +14,27 @@ export default class CustomFieldsController {
|
|||||||
const fields = await CustomField.query().where('tenant_id', tenant.id).orderBy('id', 'asc')
|
const fields = await CustomField.query().where('tenant_id', tenant.id).orderBy('id', 'asc')
|
||||||
return response.ok({ data: fields })
|
return response.ok({ data: fields })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Create custom-field metadata for contacts in the current tenant
|
||||||
|
* @requestBody {"entity_type":"contact","name":"Customer tier","key":"customer_tier","field_type":"select","required":true,"options":{"values":["bronze","silver","gold"]}}
|
||||||
|
* @responseBody 201 - {"data":{"id":1,"entity_type":"contact","name":"Customer tier","key":"customer_tier","field_type":"select","required":true,"options":{"values":["bronze","silver","gold"]},"status":"active"}}
|
||||||
|
* @responseBody 401 - {"message":"Unauthorized"}
|
||||||
|
* @responseBody 403 - {"code":"E_TENANT_ACCESS_DENIED","message":"You do not have access to this tenant"}
|
||||||
|
* @responseBody 422 - {"errors":[{"message":"The entity_type field must be contact","rule":"literal","field":"entity_type"}]}
|
||||||
|
*/
|
||||||
|
public async store({ request, response, tenant }: HttpContext) {
|
||||||
|
const input = await createCustomFieldValidator.validate(request.all())
|
||||||
|
const field = await CustomField.create({
|
||||||
|
tenantId: tenant.id,
|
||||||
|
entityType: input.entity_type,
|
||||||
|
name: input.name,
|
||||||
|
key: input.key,
|
||||||
|
fieldType: input.field_type,
|
||||||
|
required: input.required ?? false,
|
||||||
|
options: input.options ?? null,
|
||||||
|
status: 'active',
|
||||||
|
})
|
||||||
|
return response.created({ data: field })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const CustomFieldsController = () =>
|
|||||||
router
|
router
|
||||||
.group(() => {
|
.group(() => {
|
||||||
router.get('/', [CustomFieldsController, 'index'])
|
router.get('/', [CustomFieldsController, 'index'])
|
||||||
|
router.post('/', [CustomFieldsController, 'store'])
|
||||||
})
|
})
|
||||||
.prefix('/api/custom-fields')
|
.prefix('/api/custom-fields')
|
||||||
.use([middleware.auth(), middleware.tenant()])
|
.use([middleware.auth(), middleware.tenant()])
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import vine from '@vinejs/vine'
|
||||||
|
import { jsonObjectSchema } from '#validators/schemas/json_object_schema'
|
||||||
|
|
||||||
|
export const createCustomFieldValidator = vine.compile(
|
||||||
|
vine.object({
|
||||||
|
entity_type: vine.literal('contact'),
|
||||||
|
name: vine.string().trim().minLength(1),
|
||||||
|
key: vine
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.minLength(1)
|
||||||
|
.regex(/^[a-z][a-z0-9_]*$/),
|
||||||
|
field_type: vine.enum(['text', 'number', 'boolean', 'date', 'select'] as const),
|
||||||
|
required: vine.boolean().optional(),
|
||||||
|
options: jsonObjectSchema.optional(),
|
||||||
|
})
|
||||||
|
)
|
||||||
@@ -22,6 +22,21 @@ export class AdminRoleSchema extends BaseModel {
|
|||||||
declare updatedAt: DateTime | null
|
declare updatedAt: DateTime | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class AdminTenantSchema extends BaseModel {
|
||||||
|
static $columns = ['adminId', 'createdAt', 'id', 'tenantId', 'updatedAt'] as const
|
||||||
|
$columns = AdminTenantSchema.$columns
|
||||||
|
@column()
|
||||||
|
declare adminId: number
|
||||||
|
@column.dateTime({ autoCreate: true })
|
||||||
|
declare createdAt: DateTime
|
||||||
|
@column({ isPrimary: true })
|
||||||
|
declare id: number
|
||||||
|
@column()
|
||||||
|
declare tenantId: number
|
||||||
|
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||||
|
declare updatedAt: DateTime | null
|
||||||
|
}
|
||||||
|
|
||||||
export class AdminSchema extends BaseModel {
|
export class AdminSchema extends BaseModel {
|
||||||
static $columns = ['createdAt', 'email', 'firstName', 'id', 'lastName', 'password', 'status', 'updatedAt'] as const
|
static $columns = ['createdAt', 'email', 'firstName', 'id', 'lastName', 'password', 'status', 'updatedAt'] as const
|
||||||
$columns = AdminSchema.$columns
|
$columns = AdminSchema.$columns
|
||||||
@@ -68,6 +83,54 @@ export class AuthAccessTokenSchema extends BaseModel {
|
|||||||
declare updatedAt: DateTime | null
|
declare updatedAt: DateTime | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ContactSchema extends BaseModel {
|
||||||
|
static $columns = ['createdAt', 'displayName', 'email', 'id', 'phone', 'status', 'tenantId', 'updatedAt'] as const
|
||||||
|
$columns = ContactSchema.$columns
|
||||||
|
@column.dateTime({ autoCreate: true })
|
||||||
|
declare createdAt: DateTime
|
||||||
|
@column()
|
||||||
|
declare displayName: string
|
||||||
|
@column()
|
||||||
|
declare email: string | null
|
||||||
|
@column({ isPrimary: true })
|
||||||
|
declare id: number
|
||||||
|
@column()
|
||||||
|
declare phone: string | null
|
||||||
|
@column()
|
||||||
|
declare status: string
|
||||||
|
@column()
|
||||||
|
declare tenantId: number
|
||||||
|
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||||
|
declare updatedAt: DateTime | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CustomFieldSchema extends BaseModel {
|
||||||
|
static $columns = ['createdAt', 'entityType', 'fieldType', 'id', 'key', 'name', 'options', 'required', 'status', 'tenantId', 'updatedAt'] as const
|
||||||
|
$columns = CustomFieldSchema.$columns
|
||||||
|
@column.dateTime({ autoCreate: true })
|
||||||
|
declare createdAt: DateTime
|
||||||
|
@column()
|
||||||
|
declare entityType: string
|
||||||
|
@column()
|
||||||
|
declare fieldType: string
|
||||||
|
@column({ isPrimary: true })
|
||||||
|
declare id: number
|
||||||
|
@column()
|
||||||
|
declare key: string
|
||||||
|
@column()
|
||||||
|
declare name: string
|
||||||
|
@column()
|
||||||
|
declare options: any | null
|
||||||
|
@column()
|
||||||
|
declare required: boolean
|
||||||
|
@column()
|
||||||
|
declare status: string
|
||||||
|
@column()
|
||||||
|
declare tenantId: number
|
||||||
|
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||||
|
declare updatedAt: DateTime
|
||||||
|
}
|
||||||
|
|
||||||
export class PermissionSchema extends BaseModel {
|
export class PermissionSchema extends BaseModel {
|
||||||
static $columns = ['createdAt', 'id', 'name', 'slug', 'updatedAt'] as const
|
static $columns = ['createdAt', 'id', 'name', 'slug', 'updatedAt'] as const
|
||||||
$columns = PermissionSchema.$columns
|
$columns = PermissionSchema.$columns
|
||||||
@@ -111,6 +174,23 @@ export class RoleSchema extends BaseModel {
|
|||||||
declare updatedAt: DateTime | null
|
declare updatedAt: DateTime | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class TenantSchema extends BaseModel {
|
||||||
|
static $columns = ['createdAt', 'id', 'name', 'slug', 'status', 'updatedAt'] as const
|
||||||
|
$columns = TenantSchema.$columns
|
||||||
|
@column.dateTime({ autoCreate: true })
|
||||||
|
declare createdAt: DateTime
|
||||||
|
@column({ isPrimary: true })
|
||||||
|
declare id: number
|
||||||
|
@column()
|
||||||
|
declare name: string
|
||||||
|
@column()
|
||||||
|
declare slug: string
|
||||||
|
@column()
|
||||||
|
declare status: string
|
||||||
|
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||||
|
declare updatedAt: DateTime | null
|
||||||
|
}
|
||||||
|
|
||||||
export class WebComponentSchema extends BaseModel {
|
export class WebComponentSchema extends BaseModel {
|
||||||
static $columns = ['createdAt', 'data', 'id', 'pageId', 'sortOrder', 'type', 'updatedAt'] as const
|
static $columns = ['createdAt', 'data', 'id', 'pageId', 'sortOrder', 'type', 'updatedAt'] as const
|
||||||
$columns = WebComponentSchema.$columns
|
$columns = WebComponentSchema.$columns
|
||||||
|
|||||||
@@ -40,4 +40,40 @@ test.group('Custom fields API', (group) => {
|
|||||||
response.assertStatus(200)
|
response.assertStatus(200)
|
||||||
assert.equal(response.body().data[0].key, 'customer_tier')
|
assert.equal(response.body().data[0].key, 'customer_tier')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('creates a contact field owned by the resolved tenant', async ({ assert, client }) => {
|
||||||
|
const { admin, token } = await createAdminWithToken([])
|
||||||
|
const tenant = await Tenant.create({
|
||||||
|
name: 'Create Metadata GmbH',
|
||||||
|
slug: 'create-metadata-gmbh',
|
||||||
|
status: 'active',
|
||||||
|
})
|
||||||
|
await tenant.related('admins').attach([admin.id])
|
||||||
|
|
||||||
|
const response = await client
|
||||||
|
.post('/api/custom-fields')
|
||||||
|
.header('X-Tenant-Id', String(tenant.id))
|
||||||
|
.bearerToken(token)
|
||||||
|
.json({
|
||||||
|
entity_type: 'contact',
|
||||||
|
name: 'Customer tier',
|
||||||
|
key: 'customer_tier',
|
||||||
|
field_type: 'select',
|
||||||
|
required: true,
|
||||||
|
options: { values: ['bronze', 'silver', 'gold'] },
|
||||||
|
})
|
||||||
|
|
||||||
|
response.assertStatus(201)
|
||||||
|
assert.deepInclude(response.body().data, {
|
||||||
|
entity_type: 'contact',
|
||||||
|
name: 'Customer tier',
|
||||||
|
key: 'customer_tier',
|
||||||
|
field_type: 'select',
|
||||||
|
required: true,
|
||||||
|
options: { values: ['bronze', 'silver', 'gold'] },
|
||||||
|
status: 'active',
|
||||||
|
})
|
||||||
|
const created = await CustomField.find(response.body().data.id)
|
||||||
|
assert.equal(created?.tenantId, tenant.id)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
|
|
||||||
Documentation for project-specific modules
|
Documentation for project-specific modules
|
||||||
|
|
||||||
| Module | Backend | Frontend | Documentation |
|
| Module | Backend | Frontend | Documentation |
|
||||||
| ------ | ------- | -------- | ------------- |
|
| ------------- | ---------------------------------------------------------------------------------------------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| - | - | - | - |
|
| Custom Fields | `GET`/`POST` `/api/custom-fields` — authenticated, tenant-scoped metadata for contact dynamic fields | Not yet implemented | [ERP foundation next steps](../../.hermes/plans/2026-09-01_144533-erp-foundation-next-steps.md#step-5-implement-metadata-and-custom-fields-configuration) |
|
||||||
|
|||||||
Reference in New Issue
Block a user