1 import {Injectable} from '@angular/core';
2 import {Observable} from 'rxjs';
3 import {tap} from 'rxjs/operators';
4 import {IdlObject, IdlService} from './idl.service';
5 import {NetService} from './net.service';
6 import {AuthService} from './auth.service';
7 import {PcrudService} from './pcrud.service';
8 import {DbStoreService} from './db-store.service';
10 type OrgNodeOrId = number | IdlObject;
13 canHaveUsers?: boolean;
14 canHaveVolumes?: boolean;
15 opacVisible?: boolean;
20 interface OrgSettingsBatch {
24 @Injectable({providedIn: 'root'})
25 export class OrgService {
27 private orgList: IdlObject[] = [];
28 private orgTree: IdlObject; // root node + children
29 private orgMap: {[id: number]: IdlObject} = {};
30 private settingsCache: OrgSettingsBatch = {};
32 private orgTypeMap: {[id: number]: IdlObject} = {};
33 private orgTypeList: IdlObject[] = [];
36 private db: DbStoreService,
37 private net: NetService,
38 private auth: AuthService,
39 private pcrud: PcrudService
42 get(nodeOrId: OrgNodeOrId): IdlObject {
43 if (typeof nodeOrId === 'object') {
46 return this.orgMap[nodeOrId];
53 typeList(): IdlObject[] {
54 return this.orgTypeList;
57 typeMap(): {[id: number]: IdlObject} {
58 return this.orgTypeMap;
62 * Returns a list of org units that match the selected criteria.
63 * All filters must match for an org to be included in the result set.
64 * Unset filter options are ignored.
66 filterList(filter: OrgFilter, asId?: boolean): any[] {
68 this.list().forEach(org => {
70 const chu = filter.canHaveUsers;
71 if (chu && !this.canHaveUsers(org)) { return; }
72 if (chu === false && this.canHaveUsers(org)) { return; }
74 const chv = filter.canHaveVolumes;
75 if (chv && !this.canHaveVolumes(org)) { return; }
76 if (chv === false && this.canHaveVolumes(org)) { return; }
78 const ov = filter.opacVisible;
79 if (ov && !this.opacVisible(org)) { return; }
80 if (ov === false && this.opacVisible(org)) { return; }
82 if (filter.inList && !filter.inList.includes(org.id())) {
86 if (filter.notInList && filter.notInList.includes(org.id())) {
90 // All filter tests passed. Add it to the list
91 list.push(asId ? org.id() : org);
103 return this.orgList[0];
106 // list of org_unit objects or IDs for ancestors + me
107 ancestors(nodeOrId: OrgNodeOrId, asId?: boolean): any[] {
108 let node = this.get(nodeOrId);
109 if (!node) { return []; }
110 const nodes = [node];
111 while ( (node = this.get(node.parent_ou())) ) {
115 return nodes.map(n => n.id());
120 // tests that a node can have users
121 canHaveUsers(nodeOrId): boolean {
122 return this.get(nodeOrId).ou_type().can_have_users() === 't';
125 // tests that a node can have volumes
126 canHaveVolumes(nodeOrId): boolean {
130 .can_have_vols() === 't';
133 opacVisible(nodeOrId): boolean {
134 return this.get(nodeOrId).opac_visible() === 't';
137 // list of org_unit objects or IDs for me + descendants
138 descendants(nodeOrId: OrgNodeOrId, asId?: boolean): any[] {
139 const node = this.get(nodeOrId);
140 if (!node) { return []; }
142 const descend = (n) => {
144 n.children().forEach(descend);
148 return nodes.map(n => n.id());
153 // list of org_unit objects or IDs for ancestors + me + descendants
154 fullPath(nodeOrId: OrgNodeOrId, asId?: boolean): any[] {
155 const list = this.ancestors(nodeOrId, false).concat(
156 this.descendants(nodeOrId, false).slice(1));
158 return list.map(n => n.id());
163 sortTree(sortField?: string, node?: IdlObject): void {
164 if (!sortField) { sortField = 'shortname'; }
165 if (!node) { node = this.orgTree; }
167 node.children().sort((a, b) => {
168 return a[sortField]() < b[sortField]() ? -1 : 1;
171 node.children().forEach(n => this.sortTree(sortField, n));
174 absorbTree(node?: IdlObject): void {
180 this.orgMap[node.id()] = node;
181 this.orgList.push(node);
183 node.children().forEach(c => this.absorbTree(c));
187 * Grabs all of the org units from the server, chops them up into
188 * various shapes, then returns an "all done" promise.
190 fetchOrgs(): Promise<void> {
192 // Grab org types separately so we are guaranteed to fetch types
193 // that are not yet in use by an org unit.
194 return this.pcrud.retrieveAll(
195 'aout', {}, {anonymous: true, atomic: true}).toPromise()
197 this.orgTypeList = types;
198 types.forEach(t => this.orgTypeMap[Number(t.id())] = t);
200 return this.pcrud.search('aou', {parent_ou : null},
201 {flesh : -1, flesh_fields : {aou : ['children', 'ou_type']}},
213 private appendSettingsFromCache(names: string[], batch: OrgSettingsBatch) {
214 names.forEach(name => {
215 if (name in this.settingsCache) {
216 batch[name] = this.settingsCache[name];
221 // Pulls setting values from IndexedDB.
222 // Update local cache with any values found.
223 private appendSettingsFromDb(names: string[],
224 batch: OrgSettingsBatch): Promise<OrgSettingsBatch> {
226 if (names.length === 0) { return Promise.resolve(batch); }
228 return this.db.request({
231 action: 'selectWhereIn',
234 }).then(settings => {
236 // array of key => JSON-string objects
237 settings.forEach(setting => {
238 const value = JSON.parse(setting.value);
239 // propagate to local cache as well
240 batch[setting.name] = this.settingsCache[setting.name] = value;
244 }).catch(_ => batch);
247 // Add values for the list of named settings from the 'batch' to
248 // IndexedDB, copying the local cache as well.
249 private addSettingsToDb(names: string[],
250 batch: OrgSettingsBatch): Promise<OrgSettingsBatch> {
253 names.forEach(name => {
254 // Anything added to the db should also be cached locally.
255 this.settingsCache[name] = batch[name];
256 rows.push({name: name, value: JSON.stringify(batch[name])});
259 if (rows.length === 0) { return Promise.resolve(batch); }
261 return this.db.request({
264 action: 'insertOrReplace',
266 }).then(_ => batch).catch(_ => batch);
270 * Append the named settings from the network to the in-progress
271 * batch of settings. 'auth' is null for anonymous lookup.
273 private appendSettingsFromNet(orgId: number, names: string[],
274 batch: OrgSettingsBatch, auth?: string): Promise<OrgSettingsBatch> {
276 if (names.length === 0) { return Promise.resolve(batch); }
278 return this.net.request(
280 'open-ils.actor.ou_setting.ancestor_default.batch',
283 ).pipe(tap(settings => {
284 Object.keys(settings).forEach(key => {
285 const val = settings[key]; // null or hash
286 batch[key] = val ? val.value : null;
288 })).toPromise().then(_ => batch);
291 // Given a set of setting names and an in-progress settings batch,
292 // return the list of names which are not yet represented in the ,
293 // indicating their data needs to be fetched from the next layer up
294 // (cache, network, etc.).
295 settingNamesRemaining(names: string[], settings: OrgSettingsBatch): string[] {
296 return names.filter(name => !(name in settings));
299 // Returns a key/value batch of org unit settings.
300 // Cacheable settings (orgId === here) are pulled from local cache,
301 // then IndexedDB, then the network. Non-cacheable settings are
302 // fetched from the network each time.
303 settings(name: string | string[],
304 orgId?: number, anonymous?: boolean): Promise<OrgSettingsBatch> {
306 let names = [].concat(name);
307 let auth: string = null;
308 let useCache = false;
309 const batch: OrgSettingsBatch = {};
311 if (names.length === 0) { return Promise.resolve(batch); }
313 if (this.auth.user()) {
315 useCache = Number(orgId) === Number(this.auth.user().ws_ou());
317 orgId = this.auth.user().ws_ou();
321 // avoid passing auth token when anonymous is requested.
323 auth = this.auth.token();
326 } else if (!anonymous) {
327 console.warn('Attempt to fetch org setting(s)',
328 name, 'in non-anonymous mode without an authtoken');
329 return Promise.resolve({});
333 return this.appendSettingsFromNet(orgId, names, batch, auth);
336 this.appendSettingsFromCache(names, batch);
337 names = this.settingNamesRemaining(names, batch);
339 return this.appendSettingsFromDb(names, batch)
342 names = this.settingNamesRemaining(names, batch);
344 return this.appendSettingsFromNet(orgId, names, batch, auth)
345 .then(__ => this.addSettingsToDb(names, batch));
349 // remove setting values cached in the indexeddb settings table.
350 clearCachedSettings(): Promise<any> {
351 return this.db.request({