diff --git a/packages/core/src/__tests__/client.test.ts b/packages/core/src/__tests__/client.test.ts index ce52b1df02..84336d022a 100644 --- a/packages/core/src/__tests__/client.test.ts +++ b/packages/core/src/__tests__/client.test.ts @@ -17,7 +17,7 @@ import { Space } from '../classes' import { createClient } from '../client' import core from '../component' -import { TxOperations } from '../tx' +import { TxOperations } from '../operations' import { connect } from './connection' describe('client', () => { diff --git a/packages/core/src/__tests__/memdb.test.ts b/packages/core/src/__tests__/memdb.test.ts index 8ca1e32bd3..3f62e8e8f4 100644 --- a/packages/core/src/__tests__/memdb.test.ts +++ b/packages/core/src/__tests__/memdb.test.ts @@ -13,22 +13,44 @@ // limitations under the License. // -import type { Class, Obj, Ref, Doc } from '../classes' +import { Client } from '..' +import type { Class, Doc, Obj, Ref } from '../classes' import core from '../component' import { Hierarchy } from '../hierarchy' import { ModelDb, TxDb } from '../memdb' -import { SortingOrder } from '../storage' -import { TxOperations } from '../tx' +import { TxOperations } from '../operations' +import { DocumentQuery, FindOptions, SortingOrder, WithLookup } from '../storage' +import { Tx } from '../tx' import { genMinModel, test, TestMixin } from './minmodel' const txes = genMinModel() -async function createModel (): Promise<{ model: ModelDb, hierarchy: Hierarchy, txDb: TxDb }> { +class ClientModel extends ModelDb implements Client { + notify?: ((tx: Tx) => void) | undefined + + getHierarchy (): Hierarchy { + return this.hierarchy + } + + getModel (): ModelDb { + return this + } + + async findOne(_class: Ref>, query: DocumentQuery, options?: FindOptions): Promise | undefined> { + return (await this.findAll(_class, query, options)).shift() + } + + async close (): Promise { + + } +} + +async function createModel (): Promise<{ model: ClientModel, hierarchy: Hierarchy, txDb: TxDb }> { const hierarchy = new Hierarchy() for (const tx of txes) { hierarchy.tx(tx) } - const model = new ModelDb(hierarchy) + const model = new ClientModel(hierarchy) for (const tx of txes) { await model.tx(tx) } @@ -172,7 +194,7 @@ describe('memdb', () => { it('should push to array', async () => { const hierarchy = new Hierarchy() for (const tx of txes) hierarchy.tx(tx) - const model = new TxOperations(new ModelDb(hierarchy), core.account.System) + const model = new TxOperations(new ClientModel(hierarchy), core.account.System) for (const tx of txes) await model.tx(tx) const space = await model.createDoc(core.class.Space, core.space.Model, { name: 'name', @@ -190,7 +212,7 @@ describe('memdb', () => { it('limit and sorting', async () => { const hierarchy = new Hierarchy() for (const tx of txes) hierarchy.tx(tx) - const model = new TxOperations(new ModelDb(hierarchy), core.account.System) + const model = new TxOperations(new ClientModel(hierarchy), core.account.System) for (const tx of txes) await model.tx(tx) const without = await model.findAll(core.class.Space, {}) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ff3976f698..4d09032fd8 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -15,6 +15,7 @@ export * from './classes' export * from './tx' export * from './storage' +export * from './operations' export * from './utils' export * from './hierarchy' export * from './memdb' diff --git a/packages/core/src/memdb.ts b/packages/core/src/memdb.ts index e6e186b850..e289c48e12 100644 --- a/packages/core/src/memdb.ts +++ b/packages/core/src/memdb.ts @@ -27,13 +27,11 @@ import { TxProcessor } from './tx' * @public */ export abstract class MemDb extends TxProcessor { - protected readonly hierarchy: Hierarchy private readonly objectsByClass = new Map>, Doc[]>() private readonly objectById = new Map, Doc>() - constructor (hierarchy: Hierarchy) { + constructor (protected readonly hierarchy: Hierarchy) { super() - this.hierarchy = hierarchy } private getObjectsByClass (_class: Ref>): Doc[] { diff --git a/packages/core/src/operations.ts b/packages/core/src/operations.ts new file mode 100644 index 0000000000..8d07c621bb --- /dev/null +++ b/packages/core/src/operations.ts @@ -0,0 +1,196 @@ +import { Collection, DocumentUpdate, Hierarchy, MixinData, MixinUpdate, ModelDb } from '.' +import type { Account, AttachedData, AttachedDoc, Class, Data, Doc, Mixin, PropertyType, Ref, Space } from './classes' +import { Client } from './client' +import type { DocumentQuery, FindOptions, FindResult, TxResult, WithLookup } from './storage' +import { Tx, TxFactory } from './tx' +import core from './component' + +/** + * @public + * + * High Level operations with client, will create low level transactions. + * + * `notify` is not supported by TxOperations. + */ +export class TxOperations implements Omit { + readonly txFactory: TxFactory + + constructor (protected readonly client: Client, user: Ref) { + this.txFactory = new TxFactory(user) + } + + getHierarchy (): Hierarchy { + return this.client.getHierarchy() + } + + getModel (): ModelDb { + return this.client.getModel() + } + + async close (): Promise { + return await this.client.close() + } + + findAll (_class: Ref>, query: DocumentQuery, options?: FindOptions | undefined): Promise> { + return this.client.findAll(_class, query, options) + } + + async findOne (_class: Ref>, query: DocumentQuery, options?: FindOptions | undefined): Promise | undefined> { + return (await this.findAll(_class, query, options))[0] + } + + tx (tx: Tx): Promise { + return this.client.tx(tx) + } + + async createDoc ( + _class: Ref>, + space: Ref, + attributes: Data, + id?: Ref + ): Promise> { + const tx = this.txFactory.createTxCreateDoc(_class, space, attributes, id) + await this.client.tx(tx) + return tx.objectId + } + + async addCollection( + _class: Ref>, + space: Ref, + attachedTo: Ref, + attachedToClass: Ref>, + collection: string, + attributes: AttachedData

, + id?: Ref

+ ): Promise> { + const tx = this.txFactory.createTxCollectionCUD( + attachedToClass, + attachedTo, + space, + collection, + this.txFactory.createTxCreateDoc

(_class, space, attributes as unknown as Data

, id) + ) + await this.client.tx(tx) + return tx.objectId + } + + async updateCollection( + _class: Ref>, + space: Ref, + objectId: Ref

, + attachedTo: Ref, + attachedToClass: Ref>, + collection: string, + operations: DocumentUpdate

, + retrieve?: boolean + ): Promise> { + const tx = this.txFactory.createTxCollectionCUD( + attachedToClass, + attachedTo, + space, + collection, + this.txFactory.createTxUpdateDoc(_class, space, objectId, operations, retrieve) + ) + await this.client.tx(tx) + return tx.objectId + } + + async removeCollection( + _class: Ref>, + space: Ref, + objectId: Ref

, + attachedTo: Ref, + attachedToClass: Ref>, + collection: string + ): Promise> { + const tx = this.txFactory.createTxCollectionCUD( + attachedToClass, + attachedTo, + space, + collection, + this.txFactory.createTxRemoveDoc(_class, space, objectId) + ) + await this.client.tx(tx) + return tx.objectId + } + + putBag

( + _class: Ref>, + space: Ref, + objectId: Ref, + bag: string, + key: string, + value: P + ): Promise { + const tx = this.txFactory.createTxPutBag(_class, space, objectId, bag, key, value) + return this.client.tx(tx) + } + + updateDoc ( + _class: Ref>, + space: Ref, + objectId: Ref, + operations: DocumentUpdate, + retrieve?: boolean + ): Promise { + const tx = this.txFactory.createTxUpdateDoc(_class, space, objectId, operations, retrieve) + return this.client.tx(tx) + } + + removeDoc ( + _class: Ref>, + space: Ref, + objectId: Ref + ): Promise { + const tx = this.txFactory.createTxRemoveDoc(_class, space, objectId) + return this.client.tx(tx) + } + + createMixin( + objectId: Ref, + objectClass: Ref>, + objectSpace: Ref, + mixin: Ref>, + attributes: MixinData + ): Promise { + const tx = this.txFactory.createTxMixin(objectId, objectClass, objectSpace, mixin, attributes) + return this.client.tx(tx) + } + + updateMixin( + objectId: Ref, + objectClass: Ref>, + objectSpace: Ref, + mixin: Ref>, + attributes: MixinUpdate + ): Promise { + const tx = this.txFactory.createTxMixin(objectId, objectClass, objectSpace, mixin, attributes) + return this.client.tx(tx) + } + + update(doc: T, update: DocumentUpdate, retrieve?: boolean): Promise { + if (this.client.getHierarchy().isDerived(doc._class, core.class.AttachedDoc)) { + const adoc = doc as unknown as AttachedDoc + return this.updateCollection(doc._class, doc.space, adoc._id, adoc.attachedTo, adoc.attachedToClass, adoc.collection, update, retrieve) + } + return this.updateDoc(doc._class, doc.space, doc._id, update, retrieve) + } + + remove(doc: T): Promise { + if (this.client.getHierarchy().isDerived(doc._class, core.class.AttachedDoc)) { + const adoc = doc as unknown as AttachedDoc + return this.removeCollection(doc._class, doc.space, adoc._id, adoc.attachedTo, adoc.attachedToClass, adoc.collection) + } + return this.removeDoc(doc._class, doc.space, doc._id) + } + + add(parent: T, _class: Ref>, obj: AttachedData

, objId?: Ref

): Promise { + const h = this.client.getHierarchy() + const attrs = Array.from(h.getAllAttributes(parent._class).values()) + const collections = attrs.filter(a => h.isDerived(a.type._class, core.class.Collection) && h.isDerived(_class, (a.type as Collection).of)) + if (collections.length !== 1) { + throw new Error('Please use addCollection method, collection could not be detected.') + } + return this.addCollection(_class, parent.space, parent._id, parent._class, collections[0].name, obj, objId) + } +} diff --git a/packages/core/src/tx.ts b/packages/core/src/tx.ts index 5546e51897..924e286321 100644 --- a/packages/core/src/tx.ts +++ b/packages/core/src/tx.ts @@ -14,11 +14,11 @@ // import type { KeysByType } from 'simplytyped' -import type { Account, Arr, AttachedData, AttachedDoc, Class, Data, Doc, Domain, Mixin, PropertyType, Ref, Space } from './classes' +import type { Account, Arr, AttachedDoc, Class, Data, Doc, Domain, Mixin, PropertyType, Ref, Space } from './classes' import core from './component' import { _getOperator } from './operator' import { _toDoc } from './proxy' -import type { DocumentQuery, FindOptions, FindResult, Storage, TxResult, WithLookup } from './storage' +import type { TxResult } from './storage' import { generateId } from './utils' /** @@ -302,153 +302,6 @@ export abstract class TxProcessor implements WithTx { } } -/** - * @public - */ -export class TxOperations implements Storage { - readonly txFactory: TxFactory - - constructor (private readonly storage: Storage, user: Ref) { - this.txFactory = new TxFactory(user) - } - - findAll (_class: Ref>, query: DocumentQuery, options?: FindOptions | undefined): Promise> { - return this.storage.findAll(_class, query, options) - } - - async findOne (_class: Ref>, query: DocumentQuery, options?: FindOptions | undefined): Promise | undefined> { - return (await this.findAll(_class, query, options))[0] - } - - tx (tx: Tx): Promise { - return this.storage.tx(tx) - } - - async createDoc ( - _class: Ref>, - space: Ref, - attributes: Data, - id?: Ref - ): Promise> { - const tx = this.txFactory.createTxCreateDoc(_class, space, attributes, id) - await this.storage.tx(tx) - return tx.objectId - } - - async addCollection( - _class: Ref>, - space: Ref, - attachedTo: Ref, - attachedToClass: Ref>, - collection: string, - attributes: AttachedData

, - id?: Ref

- ): Promise> { - const tx = this.txFactory.createTxCollectionCUD( - attachedToClass, - attachedTo, - space, - collection, - this.txFactory.createTxCreateDoc

(_class, space, attributes as unknown as Data

, id) - ) - await this.storage.tx(tx) - return tx.objectId - } - - async updateCollection( - _class: Ref>, - space: Ref, - objectId: Ref

, - attachedTo: Ref, - attachedToClass: Ref>, - collection: string, - operations: DocumentUpdate

- ): Promise> { - const tx = this.txFactory.createTxCollectionCUD( - attachedToClass, - attachedTo, - space, - collection, - this.txFactory.createTxUpdateDoc(_class, space, objectId, operations) - ) - await this.storage.tx(tx) - return tx.objectId - } - - async removeCollection( - _class: Ref>, - space: Ref, - objectId: Ref

, - attachedTo: Ref, - attachedToClass: Ref>, - collection: string - ): Promise> { - const tx = this.txFactory.createTxCollectionCUD( - attachedToClass, - attachedTo, - space, - collection, - this.txFactory.createTxRemoveDoc(_class, space, objectId) - ) - await this.storage.tx(tx) - return tx.objectId - } - - putBag

( - _class: Ref>, - space: Ref, - objectId: Ref, - bag: string, - key: string, - value: P - ): Promise { - const tx = this.txFactory.createTxPutBag(_class, space, objectId, bag, key, value) - return this.storage.tx(tx) - } - - updateDoc ( - _class: Ref>, - space: Ref, - objectId: Ref, - operations: DocumentUpdate, - retrieve?: boolean - ): Promise { - const tx = this.txFactory.createTxUpdateDoc(_class, space, objectId, operations, retrieve) - return this.storage.tx(tx) - } - - removeDoc ( - _class: Ref>, - space: Ref, - objectId: Ref - ): Promise { - const tx = this.txFactory.createTxRemoveDoc(_class, space, objectId) - return this.storage.tx(tx) - } - - createMixin( - objectId: Ref, - objectClass: Ref>, - objectSpace: Ref, - mixin: Ref>, - attributes: MixinData - ): Promise { - const tx = this.txFactory.createTxMixin(objectId, objectClass, objectSpace, mixin, attributes) - return this.storage.tx(tx) - } - - updateMixin( - objectId: Ref, - objectClass: Ref>, - objectSpace: Ref, - mixin: Ref>, - attributes: MixinUpdate - ): Promise { - const tx = this.txFactory.createTxMixin(objectId, objectClass, objectSpace, mixin, attributes) - return this.storage.tx(tx) - } -} - /** * @public */ diff --git a/packages/presentation/src/attributes.ts b/packages/presentation/src/attributes.ts index caf3475669..81ea56804a 100644 --- a/packages/presentation/src/attributes.ts +++ b/packages/presentation/src/attributes.ts @@ -8,17 +8,14 @@ export interface KeyedAttribute { attr: AnyAttribute } -export async function updateAttribute (client: Client & TxOperations, object: Doc, _class: Ref>, attribute: KeyedAttribute, value: any): Promise { +export async function updateAttribute (client: TxOperations, object: Doc, _class: Ref>, attribute: KeyedAttribute, value: any): Promise { const doc = object const attributeKey = attribute.key const attr = attribute.attr if (client.getHierarchy().isMixin(attr.attributeOf)) { await client.updateMixin(doc._id, _class, doc.space, attr.attributeOf, { [attributeKey]: value }) - } else if (client.getHierarchy().isDerived(object._class, core.class.AttachedDoc)) { - const adoc = object as AttachedDoc - await client.updateCollection(_class, object.space, adoc._id, adoc.attachedTo, adoc.attachedToClass, adoc.collection, { [attributeKey]: value }) } else { - await client.updateDoc(_class, doc.space, doc._id, { [attributeKey]: value }) + await client.update(object, { [attributeKey]: value }) } } diff --git a/packages/presentation/src/utils.ts b/packages/presentation/src/utils.ts index a9b8e5d2df..0c43df9336 100644 --- a/packages/presentation/src/utils.ts +++ b/packages/presentation/src/utils.ts @@ -14,59 +14,30 @@ // limitations under the License. // +import core, { + AnyAttribute, ArrOf, AttachedDoc, Class, Client, Collection, Doc, DocumentQuery, + FindOptions, getCurrentAccount, Ref, RefTo, Tx, TxOperations, TxResult +} from '@anticrm/core' +import login from '@anticrm/login' +import { getMetadata } from '@anticrm/platform' +import { LiveQuery as LQ } from '@anticrm/query' import { onDestroy } from 'svelte' -import core, { - Doc, - Ref, - Class, - DocumentQuery, - FindOptions, - Client, - Hierarchy, - Tx, - getCurrentAccount, - ModelDb, - TxResult, - TxOperations, - AnyAttribute, - RefTo, - Collection, - AttachedDoc, - ArrOf -} from '@anticrm/core' -import { LiveQuery as LQ } from '@anticrm/query' -import { getMetadata } from '@anticrm/platform' - -import login from '@anticrm/login' - let liveQuery: LQ -let client: Client & TxOperations +let client: TxOperations class UIClient extends TxOperations implements Client { - constructor (private readonly client: Client, private readonly liveQuery: LQ) { + constructor (client: Client, private readonly liveQuery: LQ) { super(client, getCurrentAccount()._id) } - getHierarchy (): Hierarchy { - return this.client.getHierarchy() - } - - getModel (): ModelDb { - return this.client.getModel() - } - - async tx (tx: Tx): Promise { + override async tx (tx: Tx): Promise { // return Promise.all([super.tx(tx), this.liveQuery.tx(tx)]) as unknown as Promise return await super.tx(tx) } - - async close (): Promise { - await client.close() - } } -export function getClient (): Client & TxOperations { +export function getClient (): TxOperations { return client } diff --git a/plugins/activity-resources/src/components/Activity.svelte b/plugins/activity-resources/src/components/Activity.svelte index 02885fdfb9..0045dc9229 100644 --- a/plugins/activity-resources/src/components/Activity.svelte +++ b/plugins/activity-resources/src/components/Activity.svelte @@ -43,9 +43,7 @@ ) function onMessage (event: CustomEvent) { - client.addCollection(chunter.class.Comment, object.space, object._id, object._class, 'comments', { - message: event.detail - }) + client.add(object, chunter.class.Comment, { message: event.detail }) } let viewlets: Map diff --git a/plugins/activity-resources/src/components/utils.ts b/plugins/activity-resources/src/components/utils.ts index 250d2154f3..2e4a59ed37 100644 --- a/plugins/activity-resources/src/components/utils.ts +++ b/plugins/activity-resources/src/components/utils.ts @@ -14,7 +14,7 @@ export type TxDisplayViewlet = | undefined async function createPseudoViewlet ( - client: Client & TxOperations, + client: TxOperations, dtx: DisplayTx, label: string ): Promise { @@ -36,7 +36,7 @@ async function createPseudoViewlet ( } export async function updateViewlet ( - client: Client & TxOperations, + client: TxOperations, viewlets: Map, dtx: DisplayTx ): Promise<{ @@ -71,7 +71,7 @@ export async function updateViewlet ( async function checkInlineViewlets ( dtx: DisplayTx, viewlet: TxDisplayViewlet, - client: Client & TxOperations, + client: TxOperations, model: AttributeModel[] ): Promise<{ viewlet: TxDisplayViewlet, model: AttributeModel[] }> { if (dtx.tx._class === core.class.TxCreateDoc) { @@ -89,7 +89,7 @@ async function checkInlineViewlets ( async function createUpdateModel ( dtx: DisplayTx, - client: Client & TxOperations, + client: TxOperations, model: AttributeModel[] ): Promise { if (dtx.updateTx !== undefined) { @@ -116,7 +116,7 @@ async function createUpdateModel ( return model } -function getHiddenAttrs (client: Client & TxOperations, _class: Ref>): Set { +function getHiddenAttrs (client: TxOperations, _class: Ref>): Set { return new Set( [...client.getHierarchy().getAllAttributes(_class).entries()] .filter(([, attr]) => attr.hidden === true) @@ -124,7 +124,7 @@ function getHiddenAttrs (client: Client & TxOperations, _class: Ref>) ) } -export async function getValue (client: Client & TxOperations, m: AttributeModel, utx: any): Promise { +export async function getValue (client: TxOperations, m: AttributeModel, utx: any): Promise { const val = utx[m.key] if (client.getHierarchy().isDerived(m._class, core.class.Doc) && typeof val === 'string') { diff --git a/plugins/lead-resources/src/components/CreateLead.svelte b/plugins/lead-resources/src/components/CreateLead.svelte index 139eec8dac..3b61df220f 100644 --- a/plugins/lead-resources/src/components/CreateLead.svelte +++ b/plugins/lead-resources/src/components/CreateLead.svelte @@ -15,15 +15,14 @@ --> diff --git a/plugins/recruit-resources/src/components/CreateApplication.svelte b/plugins/recruit-resources/src/components/CreateApplication.svelte index a2b0152d07..1553efa31b 100644 --- a/plugins/recruit-resources/src/components/CreateApplication.svelte +++ b/plugins/recruit-resources/src/components/CreateApplication.svelte @@ -19,12 +19,11 @@ import { getResource, OK, Resource, Severity, Status } from '@anticrm/platform' import { Card, getClient, UserBox } from '@anticrm/presentation' import type { Applicant, Candidate } from '@anticrm/recruit' - import { calcRank, SpaceWithStates, State } from '@anticrm/task' - import task from '@anticrm/task' + import task, { calcRank, SpaceWithStates, State } from '@anticrm/task' import { Grid, Status as StatusControl } from '@anticrm/ui' + import view from '@anticrm/view' import { createEventDispatcher } from 'svelte' import recruit from '../plugin' - import view from '@anticrm/view' export let space: Ref export let candidate: Ref @@ -73,15 +72,7 @@ { state: state._id }, { sort: { rank: SortingOrder.Descending } } ) - const incResult = await client.updateDoc( - task.class.Sequence, - task.space.Sequence, - sequence._id, - { - $inc: { sequence: 1 } - }, - true - ) + const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true) const candidateInstance = await client.findOne(contact.class.Person, { _id: doc.attachedTo as Ref }) if (candidateInstance === undefined) { @@ -91,12 +82,8 @@ await client.createMixin(candidateInstance._id, candidateInstance._class, candidateInstance.space, recruit.mixin.Candidate, {}) } - await client.addCollection( + await client.add(candidateInstance, recruit.class.Applicant, - doc.space, - doc.attachedTo, - candidateInstance._class, - 'applications', { state: state._id, doneState: null, diff --git a/plugins/task-resources/src/components/todos/EditTodo.svelte b/plugins/task-resources/src/components/todos/EditTodo.svelte index aa9d16d28b..9ed0f52596 100644 --- a/plugins/task-resources/src/components/todos/EditTodo.svelte +++ b/plugins/task-resources/src/components/todos/EditTodo.svelte @@ -56,15 +56,7 @@ return } - await client.updateCollection( - item._class, - item.space, - item._id, - item.attachedTo, - item.attachedToClass, - item.collection, - ops - ) + await client.update(item, ops) } diff --git a/plugins/task-resources/src/index.ts b/plugins/task-resources/src/index.ts index 15f1cb0a3a..57db95678a 100644 --- a/plugins/task-resources/src/index.ts +++ b/plugins/task-resources/src/index.ts @@ -52,17 +52,7 @@ async function editStatuses (object: SpaceWithStates): Promise { } async function toggleDone (value: boolean, object: TodoItem): Promise { - await getClient().updateCollection( - object._class, - object.space, - object._id, - object.attachedTo, - object.attachedToClass, - object.collection, - { - done: value - } - ) + await getClient().update(object, { done: value }) } async function ArchiveSpace (object: SpaceWithStates): Promise { @@ -78,9 +68,7 @@ async function ArchiveSpace (object: SpaceWithStates): Promise { const client = getClient() // eslint-disable-next-line @typescript-eslint/no-floating-promises - client.updateDoc(object._class, object.space, object._id, { - archived: true - }) + client.update(object, { archived: true }) } } ) @@ -99,9 +87,7 @@ async function UnarchiveSpace (object: SpaceWithStates): Promise { const client = getClient() // eslint-disable-next-line @typescript-eslint/no-floating-promises - client.updateDoc(object._class, object.space, object._id, { - archived: false - }) + client.update(object, { archived: false }) } } ) diff --git a/plugins/view-resources/src/utils.ts b/plugins/view-resources/src/utils.ts index 918a921506..ae2e675907 100644 --- a/plugins/view-resources/src/utils.ts +++ b/plugins/view-resources/src/utils.ts @@ -175,7 +175,7 @@ export async function getActions (client: Client, doc: Doc, derived: Ref { +export async function deleteObject (client: TxOperations, object: Doc): Promise { const hierarchy = client.getHierarchy() const attributes = hierarchy.getAllAttributes(object._class) for (const [name, attribute] of attributes) {