sumba 2.10.0 → 2.11.0

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.
@@ -4,11 +4,22 @@ export async function rebuildFilter (modelName, filter, req) {
4
4
  const { isEmpty, isPlainObject, map, find, get } = this.app.lib._
5
5
  filter.query = filter.query ?? {}
6
6
 
7
- const queryByModel = (query) => {
8
- // by model
7
+ const queryBySiteSetting = (query) => {
8
+ if (!req.site) return
9
9
  const setting = get(req, `site.setting.dobo.query.${modelName}`)
10
10
  if (isPlainObject(setting) && !isEmpty(setting)) query.$and.push(setting)
11
- return query
11
+ }
12
+
13
+ const queryByTeamSetting = (query) => {
14
+ if (!req.user) return
15
+ const q = []
16
+ for (const team of req.user.teams) {
17
+ const item = get(team, `setting.dobo.query.${modelName}`)
18
+ if (item) q.push(item)
19
+ }
20
+ if (isEmpty(q)) return
21
+ if (q.length === 1) query.$and.push(q[0])
22
+ else query.$and.push({ $or: q })
12
23
  }
13
24
 
14
25
  const model = this.app.dobo.getModel(modelName)
@@ -17,14 +28,17 @@ export async function rebuildFilter (modelName, filter, req) {
17
28
  const hasTeamId = model.hasProperty('teamId')
18
29
  const isAdmin = find(get(req, 'user.teams', []), { alias: 'administrator' }) && useAdmin.includes(get(req, 'routeOptions.config.ns'))
19
30
  const q = { $and: [] }
31
+ queryBySiteSetting(q)
32
+ queryByTeamSetting(q)
20
33
  if (!isEmpty(filter.query)) {
21
34
  if (filter.query.$and) q.$and.push(...filter.query.$and)
22
35
  else q.$and.push(filter.query)
23
36
  }
37
+ /*
24
38
  if (!(hasSiteId || hasUserId || hasTeamId)) {
25
- filter.query = queryByModel(q)
26
39
  return filter
27
40
  }
41
+ */
28
42
  if (hasSiteId) q.$and.push({ siteId: req.site.id })
29
43
  if (hasTeamId && !isAdmin) {
30
44
  const teamIds = map(req.user.teams, 'id')
@@ -33,14 +47,13 @@ export async function rebuildFilter (modelName, filter, req) {
33
47
  } else if (!isAdmin) {
34
48
  if (hasUserId) q.$and.push({ userId: req.user.id })
35
49
  }
36
- filter.query = queryByModel(q)
37
- return filter
50
+ filter.query = q
38
51
  }
39
52
 
40
53
  export async function handler (modelName, filter, options = {}) {
41
54
  const { req } = options
42
55
  if (options.noAutoFilter || !req) return
43
- filter = await rebuildFilter.call(this, modelName, filter, req)
56
+ await rebuildFilter.call(this, modelName, filter, req)
44
57
  }
45
58
 
46
59
  const doboBeforeFindRecord = {
@@ -5,7 +5,8 @@ export async function checker (modelName, id, options = {}) {
5
5
 
6
6
  const model = this.app.dobo.getModel(modelName)
7
7
  if (options.noAutoFilter || !req) return
8
- const filter = await rebuildFilter.call(this, modelName, {}, req)
8
+ const filter = {}
9
+ await rebuildFilter.call(this, modelName, filter, req)
9
10
  if (filter.query.$and) filter.query.$and.push({ id })
10
11
  else filter.query.id = id
11
12
  filter.limit = 1
@@ -1,8 +1,7 @@
1
1
  const preParsing = {
2
2
  level: 10,
3
3
  handler: async function (req, reply) {
4
- const { getHostname } = this.app.waibu
5
- req.site = await this.getSite(getHostname(req))
4
+ req.site = await this.getSite(req)
6
5
  }
7
6
  }
8
7
 
@@ -100,6 +100,7 @@
100
100
  "manageUser": "Manage User",
101
101
  "manageTeam": "Manage Team",
102
102
  "manageTeamUser": "Manage Team Member",
103
+ "manageTeamSetting": "Manage Team Setting",
103
104
  "resetUserPassword": "Reset User Password",
104
105
  "unknownUser": "User Unknown/Invalid",
105
106
  "socialMedia": "Social Media",
@@ -101,6 +101,7 @@
101
101
  "manageUser": "Kelola Pengguna",
102
102
  "manageTeam": "Kelola Tim",
103
103
  "manageTeamUser": "Kelola Anggota Tim",
104
+ "manageTeamSetting": "Kelola Setelan Tim",
104
105
  "resetUserPassword": "Reset Kata Sandi Pengguna",
105
106
  "unknownUser": "Pengguna Tidak Dikenal/Tidak Valid",
106
107
  "socialMedia": "Media Sosial",
@@ -4,11 +4,19 @@ async function teamId (opts = {}) {
4
4
  name: 'teamId',
5
5
  type: 'string',
6
6
  maxLength: 50,
7
+ required: true,
7
8
  ref: {
8
9
  site: {
9
10
  model: 'SumbaSite',
10
11
  propName: 'id',
11
- type: '1:1'
12
+ type: '1:1',
13
+ fields: ['id', 'alias', 'hostname', 'title']
14
+ },
15
+ team: {
16
+ model: 'SumbaTeam',
17
+ propName: 'id',
18
+ type: '1:1',
19
+ fields: ['id', 'name']
12
20
  }
13
21
  },
14
22
  index: true
@@ -3,14 +3,15 @@
3
3
  "properties": [{
4
4
  "name": "hostname",
5
5
  "type": "string",
6
- "minLength": 5,
7
6
  "maxLength": 100,
7
+ "required": true,
8
8
  "index": "unique"
9
9
  }, {
10
10
  "name": "alias",
11
11
  "type": "string",
12
12
  "maxLength": 100,
13
13
  "index": "unique",
14
+ "required": true,
14
15
  "immutable": true
15
16
  }, {
16
17
  "name": "title",
@@ -20,7 +21,6 @@
20
21
  }, {
21
22
  "name": "orgName",
22
23
  "type": "string",
23
- "minLength": 5,
24
24
  "maxLength": 100,
25
25
  "index": true
26
26
  }, {
@@ -0,0 +1,17 @@
1
+ {
2
+ "buildLevel": 5,
3
+ "properties": [
4
+ "ns,,50,true,true",
5
+ "key,,255,true,true",
6
+ "value,text"
7
+ ],
8
+ "indexes": [{
9
+ "fields": ["ns", "key", "siteId", "teamId"],
10
+ "type": "unique"
11
+ }],
12
+ "features": [
13
+ "sumba:siteId",
14
+ "dobo:updatedAt",
15
+ "sumba:teamId"
16
+ ]
17
+ }
@@ -1,19 +1,6 @@
1
1
  {
2
2
  "buildLevel": 4,
3
- "properties": [{
4
- "name": "teamId",
5
- "type": "string",
6
- "maxLength": 50,
7
- "required": true,
8
- "ref": {
9
- "team": {
10
- "model": "SumbaTeam",
11
- "propName": "id",
12
- "type": "1:1",
13
- "fields": ["id", "name"]
14
- }
15
- }
16
- }],
3
+ "properties": [],
17
4
  "indexes": [{
18
5
  "fields": ["userId", "siteId", "teamId"],
19
6
  "type": "unique"
@@ -21,6 +8,7 @@
21
8
  "features": [
22
9
  "dobo:createdAt",
23
10
  "dobo:updatedAt",
11
+ "sumba:teamId",
24
12
  "sumba:siteId",
25
13
  "sumba:userId",
26
14
  "dobo:immutable"
@@ -11,7 +11,6 @@ const auth = {
11
11
  user = await this.getUser(session.userId)
12
12
  if (user) {
13
13
  socket.join(camelCase(`user ${user.username}`))
14
- await this.mergeTeam(user, site)
15
14
  for (const team of user.teams) {
16
15
  socket.join(camelCase(`team ${team.alias}`))
17
16
  }
@@ -0,0 +1,51 @@
1
+ async function teamUser () {
2
+ return {
3
+ common: {
4
+ layout: [
5
+ { name: 'meta', fields: ['id', 'teamId', 'createdAt', 'updatedAt'] },
6
+ { name: 'general', fields: ['ns', 'key', 'value'] }
7
+ ],
8
+ calcFields: [
9
+ { name: 'team', type: 'string' }
10
+ ],
11
+ valueFormatter: {
12
+ team: (val, rec) => {
13
+ return rec._ref.team.name
14
+ }
15
+ },
16
+ widget: {
17
+ teamId: {
18
+ component: 'form-select-ext',
19
+ attr: {
20
+ remoteUrl: 'sumba.restapi:/manage/team',
21
+ remoteSearchField: 'name',
22
+ remoteLabelField: 'name',
23
+ remoteApiKey: true,
24
+ ref: 'team:name'
25
+ }
26
+ }
27
+ }
28
+ },
29
+ view: {
30
+ list: {
31
+ fields: ['team', 'ns', 'key', 'value', 'createdAt', 'updatedAt'],
32
+ stat: {
33
+ aggregate: [
34
+ { fields: ['userId'], group: 'userId', aggregate: ['count'] },
35
+ { fields: ['teamId'], group: 'teamId', aggregate: ['count'] }
36
+ ]
37
+ }
38
+ },
39
+ details: {
40
+ },
41
+ add: {
42
+ hidden: ['id', 'createdAt', 'updatedAt']
43
+ },
44
+ edit: {
45
+ readonly: ['id', 'createdAt', 'updatedAt']
46
+ }
47
+ }
48
+ }
49
+ }
50
+
51
+ export default teamUser
@@ -25,7 +25,7 @@ async function teamUser () {
25
25
  remoteSearchField: 'username',
26
26
  remoteLabelField: 'username',
27
27
  remoteApiKey: true,
28
- rel: 'user:username'
28
+ ref: 'user:username'
29
29
  }
30
30
  },
31
31
  teamId: {
@@ -35,7 +35,7 @@ async function teamUser () {
35
35
  remoteSearchField: 'name',
36
36
  remoteLabelField: 'name',
37
37
  remoteApiKey: true,
38
- rel: 'team:name'
38
+ ref: 'team:name'
39
39
  }
40
40
  }
41
41
  }
@@ -0,0 +1,11 @@
1
+ const action = {
2
+ method: ['GET', 'POST'],
3
+ title: 'manageTeamSetting',
4
+ handler: async function (req, reply) {
5
+ const { importModule } = this.app.bajo
6
+ const crudSkel = await importModule('waibuAdmin:/lib/crud-skel.js')
7
+ return await crudSkel.call(this, 'SumbaTeamSetting', req, reply)
8
+ }
9
+ }
10
+
11
+ export default action
package/index.js CHANGED
@@ -2,6 +2,7 @@ import path from 'path'
2
2
  import createNewSite from './lib/create-new-site.js'
3
3
  import removeSite from './lib/remove-site.js'
4
4
  import getSite from './lib/get-site.js'
5
+ import getUser from './lib/get-user.js'
5
6
 
6
7
  /**
7
8
  * Plugin factory
@@ -11,6 +12,7 @@ import getSite from './lib/get-site.js'
11
12
  */
12
13
  async function factory (pkgName) {
13
14
  const me = this
15
+ const { getModel } = this.app.dobo
14
16
 
15
17
  /**
16
18
  * Sumba class
@@ -133,7 +135,7 @@ async function factory (pkgName) {
133
135
  }
134
136
  }
135
137
  this.unsafeUserFields = ['password']
136
- this.selfBind(['createNewSite', 'removeSite', 'getSite'])
138
+ this.selfBind(['createNewSite', 'removeSite', 'getSite', 'getUser'])
137
139
  }
138
140
 
139
141
  init = async () => {
@@ -188,6 +190,7 @@ async function factory (pkgName) {
188
190
  { title: 'manageUser', href: `waibuAdmin:/${prefix}/user/list` },
189
191
  { title: 'manageTeam', href: `waibuAdmin:/${prefix}/team/list` },
190
192
  { title: 'manageTeamUser', href: `waibuAdmin:/${prefix}/team-user/list` },
193
+ { title: 'manageTeamSetting', href: `waibuAdmin:/${prefix}/team-setting/list` },
191
194
  { title: 'manageDownload', href: `waibuAdmin:/${prefix}/download/list` },
192
195
  { title: '-' },
193
196
  { title: 'siteSetting', href: `waibuAdmin:/${prefix}/site-setting/list` },
@@ -201,37 +204,9 @@ async function factory (pkgName) {
201
204
  }]
202
205
  }
203
206
 
204
- getUser = async (rec, safe = true) => {
205
- const { omit, isPlainObject } = this.app.lib._
206
- let user
207
- if (isPlainObject(rec)) user = rec
208
- else {
209
- const mdl = this.app.dobo.getModel('SumbaUser')
210
- user = await mdl.getRecord(rec, { noHook: true, throwNotFound: false })
211
- }
212
- if (!user) return null
213
- return safe ? omit(user, this.unsafeUserFields) : user
214
- }
215
-
216
- mergeTeam = async (user, site) => {
217
- if (!user) return
218
- const { map, pick } = this.app.lib._
219
- user.teams = []
220
- const query = { userId: user.id, siteId: site.id }
221
- let mdl = this.app.dobo.getModel('SumbaTeamUser')
222
- const userTeam = await mdl.findAllRecord({ query })
223
- if (userTeam.length === 0) return
224
- delete query.userId
225
- query.id = { $in: map(userTeam, 'teamId') }
226
- query.status = 'ENABLED'
227
- mdl = this.app.dobo.getModel('SumbaTeam')
228
- const team = await mdl.findAllRecord({ query })
229
- if (team.length > 0) user.teams.push(...map(team, t => pick(t, ['id', 'alias'])))
230
- }
231
-
232
207
  getUserFromUsernamePassword = async (username = '', password = '', req) => {
233
208
  const { importPkg } = this.app.bajo
234
- const model = this.app.dobo.getModel('SumbaUser')
209
+ const model = getModel('SumbaUser')
235
210
  await model.validate({ username, password }, null, { partial: true, ns: ['sumba', 'dobo'], fields: ['username', 'password'] })
236
211
  const bcrypt = await importPkg('bajoExtra:bcrypt')
237
212
 
@@ -287,7 +262,7 @@ async function factory (pkgName) {
287
262
  if (!isMd5(token)) return false
288
263
  token = await hash(token)
289
264
  const query = { token }
290
- const rows = await this.app.dobo.getModel('SumbaUser').findRecord({ query }, { req, noHook: true })
265
+ const rows = await getModel('SumbaUser').findRecord({ query }, { req, noHook: true })
291
266
  if (rows.length === 0) throw this.error('invalidKey', merge({ statusCode: 401 }, payload))
292
267
  if (rows[0].status !== 'ACTIVE') throw this.error('userInactive', merge({ details: [{ field: 'status', error: 'inactive' }], statusCode: 401 }, payload))
293
268
  req.user = await getUser(rows[0])
@@ -353,7 +328,7 @@ async function factory (pkgName) {
353
328
  const decoded = await verifier(token)
354
329
  const id = decoded.payload.uid
355
330
  try {
356
- const rec = await this.app.dobo.getModel('SumbaUser').getRecord(id, { req, noHook: true })
331
+ const rec = await getModel('SumbaUser').getRecord(id, { req, noHook: true })
357
332
  if (!rec) throw this.error('invalidToken', { statusCode: 401 })
358
333
  if (rec.status !== 'ACTIVE') throw this.error('userInactive', { details: [{ field: 'status', error: 'inactive' }], statusCode: 401 })
359
334
  req.user = await getUser(rec)
@@ -472,7 +447,7 @@ async function factory (pkgName) {
472
447
  getApiKeyFromUserId = async id => {
473
448
  const { hash } = this.app.bajoExtra
474
449
  const options = { forceNoHidden: true, noHook: true, noCache: true, attachment: true, mimeType: true }
475
- const resp = await this.app.dobo.getModel('SumbaUser').getRecord(id, options)
450
+ const resp = await getModel('SumbaUser').getRecord(id, options)
476
451
  return await hash(resp.salt)
477
452
  }
478
453
 
@@ -519,6 +494,7 @@ async function factory (pkgName) {
519
494
  createNewSite = createNewSite
520
495
  removeSite = removeSite
521
496
  getSite = getSite
497
+ getUser = getUser
522
498
  }
523
499
 
524
500
  return Sumba
package/lib/check-team.js CHANGED
@@ -3,7 +3,6 @@ import { pathsToCheck } from './check-user-id.js'
3
3
  async function checkTeam (req, reply, source) {
4
4
  if (!req.user) return
5
5
  const { map } = this.app.lib._
6
- await this.mergeTeam(req.user, req.site)
7
6
  const paths = pathsToCheck.call(this, req, true)
8
7
  const teams = map(req.user.teams, 'alias')
9
8
  const match = this.checkPathsByTeam({ paths, method: req.method, teams, guards: this.teamRoutes })
package/lib/get-site.js CHANGED
@@ -1,11 +1,15 @@
1
- async function getSite (hostname, useId) {
2
- const { omit } = this.app.lib._
1
+ import { parseNsSettings } from './util.js'
2
+
3
+ async function getSite (req) {
4
+ const { runHook } = this.app.bajo
5
+ const { omit, isPlainObject } = this.app.lib._
6
+ const { getHostname } = this.app.waibu
3
7
  const omitted = ['status']
4
8
 
9
+ await runHook(`${this.ns}:beforeGetSite`, req)
5
10
  const mergeSetting = async (site) => {
6
11
  const { defaultsDeep, isSet } = this.app.lib.aneka
7
- const { parseObject, dayjs } = this.app.lib
8
- const { isEmpty, trim, get, filter, set, isPlainObject, isArray } = this.app.lib._
12
+ const { get, filter } = this.app.lib._
9
13
  const defSetting = {}
10
14
  const nsSetting = {}
11
15
  const names = this.app.getAllNs()
@@ -18,27 +22,13 @@ async function getSite (hostname, useId) {
18
22
  const item = get(this, `app.${ns}.config.siteSetting`)
19
23
  if (isSet(item)) defSetting[ns] = item
20
24
  const items = filter(all, { ns })
21
- for (const item of items) {
22
- let value = trim([item.value] ?? '')
23
- if (['[', '{'].includes(value[0])) {
24
- try {
25
- value = parseObject(JSON.parse(value))
26
- } catch (err) {}
27
- } else if (Number(value)) value = Number(value)
28
- else if (['true', 'false'].includes(value)) value = value === 'true'
29
- else if (item.key.endsWith('$in')) value = value.split(',').map(v => v.trim())
30
- else {
31
- const dt = dayjs(value)
32
- if (dt.isValid()) value = dt.toDate()
33
- }
34
- if ((isPlainObject(value) || isArray(value)) && isEmpty(value)) continue
35
- set(nsSetting, `${ns}.${item.key}`, value)
36
- }
25
+ parseNsSettings.call(this, ns, nsSetting, items)
37
26
  }
38
27
  site.setting = defaultsDeep({}, nsSetting, defSetting)
39
28
  // additional fields
40
29
  const country = await this.app.dobo.getModel('CdbCountry').getRecord(site.country, { noHook: true })
41
30
  site.countryName = (country ?? {}).name ?? site.country
31
+ await runHook(`${this.ns}:afterGetSite`, req, site)
42
32
  }
43
33
 
44
34
  let site = {}
@@ -51,8 +41,8 @@ async function getSite (hostname, useId) {
51
41
  return site
52
42
  }
53
43
  let query
54
- if (useId) query = { id: hostname }
55
- else query = { hostname }
44
+ if (!isPlainObject(req)) query = { id: req }
45
+ else query = { hostname: getHostname(req) }
56
46
  let row = await this.app.dobo.getModel('SumbaSite').findOneRecord({ query }, { noHook: true })
57
47
  if (!row) {
58
48
  if (multiSite.catchAll) {
@@ -0,0 +1,50 @@
1
+ import { parseNsSettings } from './util.js'
2
+
3
+ async function getUser (rec, safe = true) {
4
+ const { runHook } = this.app.bajo
5
+ const { map, pick, omit, isPlainObject } = this.app.lib._
6
+ const { getModel } = this.app.dobo
7
+ await runHook(`${this.ns}:beforeGetUser`, rec, safe)
8
+ let user
9
+ if (!isPlainObject(rec)) {
10
+ const mdl = getModel('SumbaUser')
11
+ user = await mdl.getRecord(rec, { noHook: true, throwNotFound: false })
12
+ } else {
13
+ user = rec
14
+ }
15
+ if (!user) return null
16
+ // merge teams
17
+ user.teams = []
18
+ const query = { userId: user.id, siteId: user.siteId }
19
+ let mdl = getModel('SumbaTeamUser')
20
+ const userTeam = await mdl.findAllRecord({ query })
21
+ if (userTeam.length === 0) return
22
+ delete query.userId
23
+ query.id = { $in: map(userTeam, 'teamId') }
24
+ query.status = 'ENABLED'
25
+ mdl = getModel('SumbaTeam')
26
+ const teams = await mdl.findAllRecord({ query })
27
+ if (teams.length > 0) {
28
+ // setting
29
+ delete query.id
30
+ delete query.status
31
+ query.siteId = user.siteId
32
+ query.teamId = { $in: teams.map(t => t.id + '') }
33
+ mdl = getModel('SumbaTeamSetting')
34
+ const items = await mdl.findAllRecord({ query })
35
+ for (const team of teams) {
36
+ const names = map(items, 'ns')
37
+ const item = pick(team, ['id', 'alias'])
38
+ item.setting = {}
39
+ for (const ns of names) {
40
+ parseNsSettings.call(this, ns, item.setting, items.filter(s => s.teamId === (team.id + '')))
41
+ }
42
+ user.teams.push(item)
43
+ }
44
+ }
45
+ user = safe ? omit(user, this.unsafeUserFields) : user
46
+ await runHook(`${this.ns}:afterGetUser`, rec, safe, user)
47
+ return user
48
+ }
49
+
50
+ export default getUser
package/lib/util.js ADDED
@@ -0,0 +1,21 @@
1
+ export function parseNsSettings (ns, setting, items) {
2
+ const { trim, set, isPlainObject, isArray, isEmpty } = this.app.lib._
3
+ const { parseObject, dayjs } = this.app.lib
4
+
5
+ for (const item of items) {
6
+ let value = trim([item.value] ?? '')
7
+ if (['[', '{'].includes(value[0])) {
8
+ try {
9
+ value = parseObject(JSON.parse(value))
10
+ } catch (err) {}
11
+ } else if (Number(value)) value = Number(value)
12
+ else if (['true', 'false'].includes(value)) value = value === 'true'
13
+ else if (item.key.endsWith('$in')) value = value.split(',').map(v => v.trim())
14
+ else {
15
+ const dt = dayjs(value)
16
+ if (dt.isValid()) value = dt.toDate()
17
+ }
18
+ if ((isPlainObject(value) || isArray(value)) && isEmpty(value)) continue
19
+ set(setting, `${ns}.${item.key}`, value)
20
+ }
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sumba",
3
- "version": "2.10.0",
3
+ "version": "2.11.0",
4
4
  "description": "Biz Suite for Bajo Framework",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/wiki/CHANGES.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-03-22
4
+
5
+ - [2.11.0] Add ```Team Setting``` feature
6
+ - [2.11.0] Rewrite ```getUser()```
7
+ - [2.11.0] Rewrite ```getSite()```
8
+ - [2.11.0] Bug fix in model reference not displayed correctly on ```Details View```
9
+
3
10
  ## 2026-03-13
4
11
 
5
12
  - [2.10.0] ```getSite()``` now accept object & array based on their keys to