@things-factory/contact 8.0.0-beta.9 → 8.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/contact",
3
- "version": "8.0.0-beta.9",
3
+ "version": "8.0.2",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "dist-client/index.js",
6
6
  "things-factory": true,
@@ -27,14 +27,14 @@
27
27
  "migration:create": "node ../../node_modules/typeorm/cli.js migration:create ./server/migrations/migration"
28
28
  },
29
29
  "dependencies": {
30
- "@operato/contact": "^8.0.0-beta",
31
- "@operato/graphql": "^8.0.0-beta",
32
- "@operato/i18n": "^8.0.0-beta",
33
- "@operato/shell": "^8.0.0-beta",
34
- "@operato/styles": "^8.0.0-beta",
35
- "@things-factory/attachment-base": "^8.0.0-beta.9",
36
- "@things-factory/auth-base": "^8.0.0-beta.9",
37
- "@things-factory/shell": "^8.0.0-beta.9"
30
+ "@operato/contact": "^8.0.0",
31
+ "@operato/graphql": "^8.0.0",
32
+ "@operato/i18n": "^8.0.0",
33
+ "@operato/shell": "^8.0.0",
34
+ "@operato/styles": "^8.0.0",
35
+ "@things-factory/attachment-base": "^8.0.2",
36
+ "@things-factory/auth-base": "^8.0.2",
37
+ "@things-factory/shell": "^8.0.2"
38
38
  },
39
- "gitHead": "86b1dfa26292926a2d5447fdc23bfa5a9e983245"
39
+ "gitHead": "39d60f56e142561233ddf6d47b539c637971357c"
40
40
  }
@@ -0,0 +1 @@
1
+ export * from './register-contact-as-system-user'
@@ -0,0 +1,88 @@
1
+ import { ILike } from 'typeorm'
2
+ import { Role, User, UserStatus } from '@things-factory/auth-base'
3
+ import { getRepository } from '@things-factory/shell'
4
+ import { config } from '@things-factory/env'
5
+ import { Contact, ContactField } from '../service/contact/contact'
6
+
7
+ const { defaultPassword } = config.get('password')
8
+
9
+ export async function registerContactAsSystemUser(
10
+ { contactId, roleName }: { contactId: string; roleName?: string },
11
+ context: ResolverContext
12
+ ) {
13
+ const { domain, user, tx } = context.state
14
+
15
+ const contactRepository = getRepository(Contact, tx)
16
+
17
+ const contact = await contactRepository.findOne({
18
+ where: {
19
+ id: contactId,
20
+ domain: { id: domain.id }
21
+ }
22
+ })
23
+
24
+ if (!contact) {
25
+ throw new Error(context.t('error.contact-not-found', { contactId }))
26
+ }
27
+
28
+ const email = contact.getContactItem(ContactField.Email, 'work')
29
+
30
+ if (!email) {
31
+ throw new Error(context.t('error.contact-email-not-set', { contactId }))
32
+ }
33
+
34
+ const userRepository = getRepository(User, tx)
35
+ const existingUser = await userRepository.findOne({
36
+ where: { email: ILike(email) },
37
+ relations: ['domains', 'roles']
38
+ })
39
+
40
+ if (existingUser && !existingUser.domains.find(d => d.id === domain.id)) {
41
+ existingUser.domains = [...existingUser.domains, domain]
42
+ }
43
+
44
+ if (!existingUser && !defaultPassword) {
45
+ throw new Error(context.t('error.contact-initial-password-required'))
46
+ }
47
+
48
+ const salt = !existingUser && User.generateSalt()
49
+
50
+ const newUser: Partial<User> = existingUser
51
+ ? existingUser
52
+ : {
53
+ name: contact.name,
54
+ email,
55
+ userType: 'user',
56
+ domains: [domain],
57
+ status: UserStatus.ACTIVATED,
58
+ salt,
59
+ passwordUpdatedAt: new Date(),
60
+ password: User.encode(defaultPassword, salt),
61
+ updater: user,
62
+ creator: user
63
+ }
64
+
65
+ if (roleName) {
66
+ const roleRepository = getRepository(Role, tx)
67
+ const role = await roleRepository.findOne({
68
+ where: {
69
+ name: roleName,
70
+ domain: { id: domain.id }
71
+ }
72
+ })
73
+
74
+ if (!role) {
75
+ throw new Error(context.t('error.contact-role-not-found', { roleName }))
76
+ }
77
+
78
+ if (newUser.roles) {
79
+ if (!newUser.roles.find(role => role.name == roleName)) {
80
+ newUser.roles = [...newUser.roles, role]
81
+ }
82
+ } else {
83
+ newUser.roles = [role]
84
+ }
85
+ }
86
+
87
+ return await userRepository.save(newUser)
88
+ }
@@ -0,0 +1,2 @@
1
+ export * from './controllers'
2
+ export * from './service'
File without changes
@@ -0,0 +1,27 @@
1
+ import { ObjectType, Field, InputType } from 'type-graphql'
2
+
3
+ import { ContactField } from './contact'
4
+
5
+ @ObjectType()
6
+ export class ContactItem {
7
+ @Field()
8
+ label: string
9
+
10
+ @Field()
11
+ type: ContactField
12
+
13
+ @Field()
14
+ value: string
15
+ }
16
+
17
+ @InputType()
18
+ export class ContactItemPatch {
19
+ @Field()
20
+ label: string
21
+
22
+ @Field()
23
+ type: ContactField
24
+
25
+ @Field()
26
+ value: string
27
+ }
@@ -0,0 +1,181 @@
1
+ import { Resolver, Mutation, Arg, Ctx, Directive } from 'type-graphql'
2
+ import { In } from 'typeorm'
3
+
4
+ import { createAttachment, deleteAttachmentsByRef } from '@things-factory/attachment-base'
5
+
6
+ import { Contact } from './contact'
7
+ import { NewContact, ContactPatch } from './contact-type'
8
+
9
+ @Resolver(Contact)
10
+ export class ContactMutation {
11
+ @Directive('@transaction')
12
+ @Mutation(returns => Contact, { description: 'To create new Contact' })
13
+ async createContact(@Arg('contact') contact: NewContact, @Ctx() context: ResolverContext): Promise<Contact> {
14
+ const { domain, user, tx } = context.state
15
+ const file = contact.profile?.file
16
+
17
+ if (contact.profile) {
18
+ const { left, top, zoom } = contact.profile
19
+ contact.profile = { left, top, zoom }
20
+ }
21
+
22
+ const ContactRepository = tx.getRepository(Contact)
23
+
24
+ const result = await ContactRepository.save({
25
+ ...contact,
26
+ domain,
27
+ creator: user,
28
+ updater: user
29
+ })
30
+
31
+ if (file) {
32
+ await createAttachment(
33
+ null,
34
+ {
35
+ attachment: {
36
+ file,
37
+ refType: Contact.name,
38
+ refBy: result.id
39
+ }
40
+ },
41
+ context
42
+ )
43
+ }
44
+
45
+ return result
46
+ }
47
+
48
+ @Directive('@transaction')
49
+ @Mutation(returns => Contact, { description: 'To modify Contact information' })
50
+ async updateContact(
51
+ @Arg('id') id: string,
52
+ @Arg('patch') patch: ContactPatch,
53
+ @Ctx() context: ResolverContext
54
+ ): Promise<Contact> {
55
+ const { domain, user, tx } = context.state
56
+ const file = patch.profile?.file
57
+
58
+ if (patch.profile) {
59
+ const { left, top, zoom } = patch.profile
60
+ patch.profile = { left, top, zoom } as any
61
+ }
62
+
63
+ const ContactRepository = tx.getRepository(Contact)
64
+
65
+ const contact = await ContactRepository.findOne({
66
+ where: { domain: { id: domain.id }, id }
67
+ })
68
+
69
+ const result = await ContactRepository.save({
70
+ ...contact,
71
+ ...patch,
72
+ updater: user
73
+ })
74
+
75
+ if (file) {
76
+ await deleteAttachmentsByRef(null, { refBys: [result.id] }, context)
77
+ await createAttachment(
78
+ null,
79
+ {
80
+ attachment: {
81
+ file,
82
+ refType: Contact.name,
83
+ refBy: result.id
84
+ }
85
+ },
86
+ context
87
+ )
88
+ }
89
+
90
+ return result
91
+ }
92
+
93
+ @Directive('@transaction')
94
+ @Mutation(returns => [Contact], { description: "To modify multiple Contacts' information" })
95
+ async updateMultipleContact(
96
+ @Arg('patches', type => [ContactPatch]) patches: ContactPatch[],
97
+ @Ctx() context: ResolverContext
98
+ ): Promise<Contact[]> {
99
+ const { domain, user, tx } = context.state
100
+
101
+ let results = []
102
+ const _createRecords = patches.filter((patch: any) => patch.cuFlag.toUpperCase() === '+')
103
+ const _updateRecords = patches.filter((patch: any) => patch.cuFlag.toUpperCase() === 'M')
104
+ const contactRepo = tx.getRepository(Contact)
105
+
106
+ if (_createRecords.length > 0) {
107
+ for (let i = 0; i < _createRecords.length; i++) {
108
+ const newRecord = _createRecords[i]
109
+
110
+ const result = await contactRepo.save({
111
+ ...newRecord,
112
+ domain,
113
+ creator: user,
114
+ updater: user
115
+ })
116
+
117
+ results.push({ ...result, cuFlag: '+' })
118
+ }
119
+ }
120
+
121
+ if (_updateRecords.length > 0) {
122
+ for (let i = 0; i < _updateRecords.length; i++) {
123
+ const updateRecord = _updateRecords[i]
124
+ const contact = await contactRepo.findOneBy({ id: updateRecord.id })
125
+
126
+ const result = await contactRepo.save({
127
+ ...contact,
128
+ ...updateRecord,
129
+ updater: user
130
+ })
131
+
132
+ results.push({ ...result, cuFlag: 'M' })
133
+ }
134
+ }
135
+
136
+ return results
137
+ }
138
+
139
+ @Directive('@transaction')
140
+ @Mutation(returns => Boolean, { description: 'To delete Contact' })
141
+ async deleteContact(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<boolean> {
142
+ const { domain, tx } = context.state
143
+
144
+ await tx.getRepository(Contact).softDelete({ domain: { id: domain.id }, id })
145
+ // await deleteAttachmentsByRef(null, { refBys: [id] }, context)
146
+
147
+ return true
148
+ }
149
+
150
+ @Directive('@transaction')
151
+ @Mutation(returns => Boolean, { description: 'To delete multiple Contacts' })
152
+ async deleteContacts(@Arg('ids', type => [String]) ids: string[], @Ctx() context: ResolverContext): Promise<boolean> {
153
+ const { domain, tx } = context.state
154
+
155
+ await tx.getRepository(Contact).softDelete({
156
+ domain: { id: domain.id },
157
+ id: In(ids)
158
+ })
159
+
160
+ // await deleteAttachmentsByRef(null, { refBys: ids }, context)
161
+
162
+ return true
163
+ }
164
+
165
+ @Directive('@transaction')
166
+ @Mutation(returns => Boolean, { description: 'To import multiple Contacts' })
167
+ async importContacts(
168
+ @Arg('contacts', type => [ContactPatch]) contacts: ContactPatch[],
169
+ @Ctx() context: ResolverContext
170
+ ): Promise<boolean> {
171
+ const { domain, tx } = context.state
172
+
173
+ await Promise.all(
174
+ contacts.map(async (contact: ContactPatch) => {
175
+ const createdContact: Contact = await tx.getRepository(Contact).save({ domain, ...contact })
176
+ })
177
+ )
178
+
179
+ return true
180
+ }
181
+ }
@@ -0,0 +1,112 @@
1
+ import { Resolver, Query, FieldResolver, Root, Args, Arg, Ctx } from 'type-graphql'
2
+ import { GraphQLEmailAddress } from 'graphql-scalars'
3
+ import { Domain, getQueryBuilderFromListParams, getRepository, ListParam } from '@things-factory/shell'
4
+ import { User } from '@things-factory/auth-base'
5
+ import { Attachment } from '@things-factory/attachment-base'
6
+ import { Contact, ContactField } from './contact'
7
+ import { ContactItem } from './contact-item'
8
+ import { Profile } from './profile'
9
+ import { ContactList } from './contact-type'
10
+
11
+ function getContactItems(contact: Contact) {
12
+ const { items } = contact
13
+
14
+ if (!items || !(items instanceof Array)) {
15
+ return []
16
+ }
17
+
18
+ return items
19
+ }
20
+
21
+ function getContactItem(contact: Contact, type: String, label: String) {
22
+ const { items } = contact
23
+
24
+ return items?.find(item => item.type === type && item.label === label)?.value
25
+ }
26
+
27
+ @Resolver(Contact)
28
+ export class ContactQuery {
29
+ @Query(returns => Contact!, { nullable: true, description: 'To fetch a Contact' })
30
+ async contact(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<Contact> {
31
+ const { domain } = context.state
32
+
33
+ return await getRepository(Contact).findOne({
34
+ where: { domain: { id: domain.id }, id }
35
+ })
36
+ }
37
+
38
+ @Query(returns => ContactList, { description: 'To fetch multiple Contacts' })
39
+ async contacts(@Args(type => ListParam) params: ListParam, @Ctx() context: ResolverContext): Promise<ContactList> {
40
+ const { domain } = context.state
41
+
42
+ const queryBuilder = getQueryBuilderFromListParams({
43
+ domain,
44
+ params,
45
+ repository: await getRepository(Contact),
46
+ searchables: ['name', 'company', 'email', 'phone']
47
+ })
48
+
49
+ const [items, total] = await queryBuilder.getManyAndCount()
50
+
51
+ return { items, total }
52
+ }
53
+
54
+ @FieldResolver(type => Profile)
55
+ async profile(@Root() contact: Contact): Promise<Profile> {
56
+ var { left, top, zoom } = contact.profile || {}
57
+
58
+ if (left === undefined || top === undefined || zoom === undefined) {
59
+ return
60
+ }
61
+
62
+ const attachment: Attachment = await getRepository(Attachment).findOne({
63
+ where: {
64
+ domain: { id: contact.domainId },
65
+ refType: Contact.name,
66
+ refBy: contact.id
67
+ }
68
+ })
69
+
70
+ return { left, top, zoom, picture: attachment?.fullpath }
71
+ }
72
+
73
+ @FieldResolver(type => String, { nullable: true })
74
+ async phone(@Root() contact: Contact): Promise<string> {
75
+ return getContactItem(contact, ContactField.Phone, 'work')
76
+ }
77
+
78
+ @FieldResolver(type => GraphQLEmailAddress, { nullable: true })
79
+ async email(@Root() contact: Contact): Promise<string> {
80
+ return getContactItem(contact, ContactField.Email, 'work')
81
+ }
82
+
83
+ @FieldResolver(type => String, { nullable: true })
84
+ async address(@Root() contact: Contact): Promise<string> {
85
+ return getContactItem(contact, ContactField.Address, 'work')
86
+ }
87
+
88
+ @FieldResolver(type => String, { nullable: true })
89
+ async department(@Root() contact: Contact): Promise<string> {
90
+ return getContactItem(contact, ContactField.Department, 'work')
91
+ }
92
+
93
+ @FieldResolver(type => [ContactItem])
94
+ async items(@Root() contact: Contact): Promise<ContactItem[]> {
95
+ return getContactItems(contact)
96
+ }
97
+
98
+ @FieldResolver(type => Domain)
99
+ async domain(@Root() contact: Contact): Promise<Domain> {
100
+ return await getRepository(Domain).findOneBy({ id: contact.domainId })
101
+ }
102
+
103
+ @FieldResolver(type => User)
104
+ async updater(@Root() contact: Contact): Promise<User> {
105
+ return await getRepository(User).findOneBy({ id: contact.updaterId })
106
+ }
107
+
108
+ @FieldResolver(type => User)
109
+ async creator(@Root() contact: Contact): Promise<User> {
110
+ return await getRepository(User).findOneBy({ id: contact.creatorId })
111
+ }
112
+ }
@@ -0,0 +1,81 @@
1
+ import { ObjectType, Field, InputType, Int, ID } from 'type-graphql'
2
+ import { GraphQLEmailAddress } from 'graphql-scalars'
3
+
4
+ import { ProfileInput } from './profile'
5
+ import { ContactItemPatch } from './contact-item'
6
+ import { Contact } from './contact'
7
+
8
+ @InputType()
9
+ export class NewContact {
10
+ @Field()
11
+ name: string
12
+
13
+ @Field({ nullable: true })
14
+ company?: string
15
+
16
+ @Field(type => GraphQLEmailAddress, { nullable: true })
17
+ email?: string
18
+
19
+ @Field({ nullable: true })
20
+ phone?: string
21
+
22
+ @Field({ nullable: true })
23
+ address?: string
24
+
25
+ @Field({ nullable: true })
26
+ department?: string
27
+
28
+ @Field({ nullable: true })
29
+ note?: string
30
+
31
+ @Field(type => ProfileInput, { nullable: true })
32
+ profile?: ProfileInput
33
+
34
+ @Field(type => [ContactItemPatch], { nullable: true })
35
+ items?: ContactItemPatch[]
36
+ }
37
+
38
+ @InputType()
39
+ export class ContactPatch {
40
+ @Field(type => ID, { nullable: true })
41
+ id?: string
42
+
43
+ @Field({ nullable: true })
44
+ name?: string
45
+
46
+ @Field({ nullable: true })
47
+ company?: string
48
+
49
+ @Field(type => GraphQLEmailAddress, { nullable: true })
50
+ email?: string
51
+
52
+ @Field({ nullable: true })
53
+ phone?: string
54
+
55
+ @Field({ nullable: true })
56
+ address?: string
57
+
58
+ @Field({ nullable: true })
59
+ department?: string
60
+
61
+ @Field({ nullable: true })
62
+ note?: string
63
+
64
+ @Field(type => ProfileInput, { nullable: true })
65
+ profile?: ProfileInput
66
+
67
+ @Field(type => [ContactItemPatch], { nullable: true })
68
+ items?: ContactItemPatch[]
69
+
70
+ @Field({ nullable: true })
71
+ cuFlag?: string
72
+ }
73
+
74
+ @ObjectType()
75
+ export class ContactList {
76
+ @Field(type => [Contact])
77
+ items: Contact[]
78
+
79
+ @Field(type => Int)
80
+ total: number
81
+ }
@@ -0,0 +1,117 @@
1
+ import {
2
+ CreateDateColumn,
3
+ UpdateDateColumn,
4
+ DeleteDateColumn,
5
+ Entity,
6
+ Index,
7
+ Column,
8
+ RelationId,
9
+ ManyToOne,
10
+ OneToMany,
11
+ PrimaryGeneratedColumn
12
+ } from 'typeorm'
13
+ import { ObjectType, Field, Int, ID, registerEnumType } from 'type-graphql'
14
+ import { GraphQLEmailAddress } from 'graphql-scalars'
15
+
16
+ import { Domain, ScalarObject } from '@things-factory/shell'
17
+ import { User } from '@things-factory/auth-base'
18
+
19
+ import { ContactItem } from './contact-item'
20
+ import { Profile } from './profile'
21
+
22
+ export enum ContactField {
23
+ Name = 'name',
24
+ JobTitle = 'job-title',
25
+ Phone = 'phone',
26
+ Address = 'address',
27
+ Email = 'email',
28
+ Birthday = 'birthday',
29
+ Profile = 'profile',
30
+ Picture = 'picture',
31
+ Department = 'department',
32
+ Company = 'company',
33
+ Homepage = 'homepage'
34
+ }
35
+
36
+ registerEnumType(ContactField, {
37
+ name: 'ContactField',
38
+ description: 'field enumeration of a contact'
39
+ })
40
+
41
+ @Entity()
42
+ @Index('ix_contact_0', (contact: Contact) => [contact.domain, contact.name])
43
+ @ObjectType({ description: 'Entity for Contact' })
44
+ export class Contact {
45
+ @PrimaryGeneratedColumn('uuid')
46
+ @Field(type => ID)
47
+ readonly id: string
48
+
49
+ @ManyToOne(type => Domain)
50
+ @Field(type => Domain)
51
+ domain?: Domain
52
+
53
+ @RelationId((contact: Contact) => contact.domain)
54
+ domainId?: string
55
+
56
+ @Column()
57
+ @Field({ nullable: true })
58
+ name?: string
59
+
60
+ @Column({ nullable: true })
61
+ @Field({ nullable: true })
62
+ company?: string
63
+
64
+ @Field(type => GraphQLEmailAddress, { nullable: true })
65
+ email?: string
66
+
67
+ @Field({ nullable: true })
68
+ phone?: string
69
+
70
+ @Field({ nullable: true })
71
+ address?: string
72
+
73
+ @Field({ nullable: true })
74
+ department?: string
75
+
76
+ @Column({ nullable: true })
77
+ @Field({ nullable: true })
78
+ note?: string
79
+
80
+ @Column('simple-json', { nullable: true })
81
+ @Field(type => Profile, { nullable: true })
82
+ profile: Profile
83
+
84
+ @Column('simple-json', { nullable: true })
85
+ @Field(type => [ContactItem], { nullable: true })
86
+ items?: ContactItem[]
87
+
88
+ @CreateDateColumn()
89
+ @Field({ nullable: true })
90
+ createdAt?: Date
91
+
92
+ @UpdateDateColumn()
93
+ @Field({ nullable: true })
94
+ updatedAt?: Date
95
+
96
+ @DeleteDateColumn()
97
+ @Field({ nullable: true })
98
+ deletedAt?: Date
99
+
100
+ @ManyToOne(type => User, { nullable: true })
101
+ @Field(type => User, { nullable: true })
102
+ creator?: User
103
+
104
+ @RelationId((contact: Contact) => contact.creator)
105
+ creatorId?: string
106
+
107
+ @ManyToOne(type => User, { nullable: true })
108
+ @Field(type => User, { nullable: true })
109
+ updater?: User
110
+
111
+ @RelationId((contact: Contact) => contact.updater)
112
+ updaterId?: string
113
+
114
+ getContactItem(type: ContactField, label?: string) {
115
+ return (this.items || []).find(item => item.type === type && (!item.label || item.label === label))?.value
116
+ }
117
+ }
@@ -0,0 +1,7 @@
1
+ import { Contact } from './contact'
2
+ import { Profile } from './profile'
3
+ import { ContactQuery } from './contact-query'
4
+ import { ContactMutation } from './contact-mutation'
5
+
6
+ export const entities = [Profile, Contact]
7
+ export const resolvers = [ContactQuery, ContactMutation]
@@ -0,0 +1,36 @@
1
+ import { ObjectType, Field, InputType } from 'type-graphql'
2
+ import type { FileUpload } from 'graphql-upload/GraphQLUpload.js'
3
+ import GraphQLUpload from 'graphql-upload/GraphQLUpload.js'
4
+
5
+ @ObjectType({ description: 'Object type for Profile' })
6
+ export class Profile {
7
+ @Field({ nullable: true })
8
+ left?: number
9
+
10
+ @Field({ nullable: true })
11
+ top?: number
12
+
13
+ @Field({ nullable: true })
14
+ zoom?: number
15
+
16
+ @Field({ nullable: true })
17
+ picture?: string
18
+ }
19
+
20
+ @InputType({ description: 'Input type for Profile' })
21
+ export class ProfileInput {
22
+ @Field({ nullable: true })
23
+ left?: number
24
+
25
+ @Field({ nullable: true })
26
+ top?: number
27
+
28
+ @Field({ nullable: true })
29
+ zoom?: number
30
+
31
+ @Field({ nullable: true })
32
+ picture?: string
33
+
34
+ @Field(type => GraphQLUpload, { nullable: true })
35
+ file?: FileUpload
36
+ }