Evergreen-commits
evergreen-commits@list.evergreen-ils.orgApril 2025
- 1 participants
- 170 discussions

[GIT] Evergreen ILS branch main updated. 15d238e39f776db29dab169c6c200bfeb6ade674
by Git User 17 Apr '25
by Git User 17 Apr '25
17 Apr '25
This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".
The branch, main has been updated
via 15d238e39f776db29dab169c6c200bfeb6ade674 (commit)
from 7425bd671b68a186c9335c23d9074e39da4016fa (commit)
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
- Log -----------------------------------------------------------------
commit 15d238e39f776db29dab169c6c200bfeb6ade674
Author: spmorrison <101879901+spmorrison(a)users.noreply.github.com>
Date: Fri Apr 4 14:13:06 2025 -0400
Docs: Update "org unit" terminology in custom_ou_trees.adoc
Changed "org unit" to "organizational unit," except for where "org unit" is part of the interface or button name.
Signed-off-by: Jane Sandberg <js7389(a)princeton.edu>
diff --git a/docs/modules/admin_initial_setup/pages/custom_ou_trees.adoc b/docs/modules/admin_initial_setup/pages/custom_ou_trees.adoc
index bd1f38e814..a5894ba408 100644
--- a/docs/modules/admin_initial_setup/pages/custom_ou_trees.adoc
+++ b/docs/modules/admin_initial_setup/pages/custom_ou_trees.adoc
@@ -1,11 +1,11 @@
-= Custom Org Unit Trees =
+= Custom Organizational Unit Trees =
:toc:
indexterm:[Custom Organizational Unit Trees, Custom OU Trees, OPAC]
-The Custom Org Unit Trees interface is available at *Administration -> Server Administration -> Custom Org Unit Trees*.
+The Custom Organizational Unit Trees interface is available at *Administration -> Server Administration -> Custom Org Unit Trees*.
-This interface allows you to create a custom display of organizational units that will display in the Library dropdown in the OPAC. You can move org units up or down in the display, nest them under a different unit than their parent unit, or hide an org unit from OPAC display.
+This interface allows you to create a custom display of organizational units that will display in the Library dropdown in the OPAC. You can move organizational units up or down in the display, nest them under a different unit than their parent unit, or hide an organizational unit from the OPAC display.
====
NOTE: Your Evergreen System Administrator will need to reload Apache in order for your changes to be reflected in the OPAC.
@@ -13,34 +13,34 @@ NOTE: Your Evergreen System Administrator will need to reload Apache in order fo
image::custom_ou_trees/custom_ou_trees.png[Custom Org Unit Trees interface]
-The left side of the interface represents the full organizational tree, and is labeled _Full Org Unit Tree_. The hierarchy of the tree can be thought of as parent-child relationships. In terms of org units, parents can have several children (which would be considered “sibling” org units to each other), but a child can only have one parent. Stock Evergreen contains 4 org unit tree levels:
+The left side of the interface represents the full organizational tree, and is labeled *Full Org Unit Tree*. The hierarchy of the tree can be thought of as parent-child relationships. In terms of organizational units, parents can have several children (which would be considered “sibling” organizational units to each other), but a child can only have one parent. Stock Evergreen contains four (4) organizational unit tree levels:
* CONS - Consortium
* SYS - System
* BR - Branch
* SL / BM - Sub-library / Bookmobile
-The right side of the interface represents your custom organizational tree, and is labeled _Custom Org Unit Tree_.
+The right side of the interface represents your custom organizational tree, and is labeled *Custom Org Unit Tree*.
-To create a custom org unit tree:
+To create a custom organizational unit tree:
-. Select one or several org units from the tree on the left.
-. Select a destination org unit in the tree on the right.
-. Click the button _Copy Selected Org Units to Custom Tree_
+. Select one or several organizational units from the tree on the left.
+. Select a destination organizational unit in the tree on the right.
+. Click the button *Copy Selected Org Units to Custom Tree*.
+
.. If you copy a parent/child pair from the tree on the left, this parent/child relationship will persist in the tree on the right.
-. If you click on an org unit on the right-hand tree you will see several icons:
+. If you click on an organizational unit on the right-hand tree, you will see several icons:
+
image::custom_ou_trees/custom_ou_trees_options.png[Icons to move Org Units]
+
-.. Up arrow - this moves the org unit up in the display order, within a set of sibling organizational units. I.e., this action will not move an org unit to a new parent org unit.
-.. Down arrow - this moves the org unit down in the display order, within a set of sibling organizational units. I.e., this action will not move an org unit to a new parent org unit.
-.. Right arrow - this will open a modal _Move Org Unit Elsewhere_
-... Within this modal, select a new parent for the org unit and then click Move Org Unit Here. This will move the org unit and any of its descendants to a new parent org unit.
+.. Up arrow - this moves the organizational unit up in the display order, within a set of sibling organizational units; i.e., this action will not move an organizational unit to a new parent organizational unit.
+.. Down arrow - this moves the organizational unit down in the display order, within a set of sibling organizational units; i.e., this action will not move an organizational unit to a new parent organizational unit.
+.. Right arrow - this will open a modal *Move Org Unit Elsewhere*.
+... Within this modal, select a new parent for the organizational unit and then click *Move Org Unit Here*. This will move the organizational unit and any of its descendants to a new parent organizational unit.
+
image::custom_ou_trees/custom_ou_trees_move.png[Moving an Org Unit]
+
-.. Trashcan - this will delete the selected org unit from the custom org unit tree.
-. Once you have finished making your changes, click _Save Changes_.
+.. Trash can - this will delete the selected organizational unit from the custom organizational unit tree.
+. Once you have finished making your changes, click *Save Changes*.
-When you are ready for the tree to be OPAC-visible, select _Activate Tree_ and then _Save Changes_. You will need to notify your System Administrator to reload Apache in order for your changes to be reflected in the OPAC. Only one Custom Org Unit Tree can be active at a time.
\ No newline at end of file
+When you are ready for the tree to be OPAC-visible, select *Activate Tree* and then *Save Changes*. You will need to notify your System Administrator to reload Apache in order for your changes to be reflected in the OPAC. Only one custom organizational unit tree can be active at a time.
-----------------------------------------------------------------------
Summary of changes:
.../admin_initial_setup/pages/custom_ou_trees.adoc | 34 +++++++++++-----------
1 file changed, 17 insertions(+), 17 deletions(-)
hooks/post-receive
--
Evergreen ILS
1
0

[GIT] Evergreen ILS branch rel_3_14 updated. cc5f79f88729f47e1d4605647a4ade39bb01be2b
by Git User 17 Apr '25
by Git User 17 Apr '25
17 Apr '25
This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".
The branch, rel_3_14 has been updated
via cc5f79f88729f47e1d4605647a4ade39bb01be2b (commit)
from dfdc605357bcefa2434787d66b29d6b430b875ce (commit)
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
- Log -----------------------------------------------------------------
commit cc5f79f88729f47e1d4605647a4ade39bb01be2b
Author: spmorrison <101879901+spmorrison(a)users.noreply.github.com>
Date: Fri Apr 4 14:13:06 2025 -0400
Docs: Update "org unit" terminology in custom_ou_trees.adoc
Changed "org unit" to "organizational unit," except for where "org unit" is part of the interface or button name.
Signed-off-by: Jane Sandberg <js7389(a)princeton.edu>
diff --git a/docs/modules/admin_initial_setup/pages/custom_ou_trees.adoc b/docs/modules/admin_initial_setup/pages/custom_ou_trees.adoc
index bd1f38e814..a5894ba408 100644
--- a/docs/modules/admin_initial_setup/pages/custom_ou_trees.adoc
+++ b/docs/modules/admin_initial_setup/pages/custom_ou_trees.adoc
@@ -1,11 +1,11 @@
-= Custom Org Unit Trees =
+= Custom Organizational Unit Trees =
:toc:
indexterm:[Custom Organizational Unit Trees, Custom OU Trees, OPAC]
-The Custom Org Unit Trees interface is available at *Administration -> Server Administration -> Custom Org Unit Trees*.
+The Custom Organizational Unit Trees interface is available at *Administration -> Server Administration -> Custom Org Unit Trees*.
-This interface allows you to create a custom display of organizational units that will display in the Library dropdown in the OPAC. You can move org units up or down in the display, nest them under a different unit than their parent unit, or hide an org unit from OPAC display.
+This interface allows you to create a custom display of organizational units that will display in the Library dropdown in the OPAC. You can move organizational units up or down in the display, nest them under a different unit than their parent unit, or hide an organizational unit from the OPAC display.
====
NOTE: Your Evergreen System Administrator will need to reload Apache in order for your changes to be reflected in the OPAC.
@@ -13,34 +13,34 @@ NOTE: Your Evergreen System Administrator will need to reload Apache in order fo
image::custom_ou_trees/custom_ou_trees.png[Custom Org Unit Trees interface]
-The left side of the interface represents the full organizational tree, and is labeled _Full Org Unit Tree_. The hierarchy of the tree can be thought of as parent-child relationships. In terms of org units, parents can have several children (which would be considered “sibling” org units to each other), but a child can only have one parent. Stock Evergreen contains 4 org unit tree levels:
+The left side of the interface represents the full organizational tree, and is labeled *Full Org Unit Tree*. The hierarchy of the tree can be thought of as parent-child relationships. In terms of organizational units, parents can have several children (which would be considered “sibling” organizational units to each other), but a child can only have one parent. Stock Evergreen contains four (4) organizational unit tree levels:
* CONS - Consortium
* SYS - System
* BR - Branch
* SL / BM - Sub-library / Bookmobile
-The right side of the interface represents your custom organizational tree, and is labeled _Custom Org Unit Tree_.
+The right side of the interface represents your custom organizational tree, and is labeled *Custom Org Unit Tree*.
-To create a custom org unit tree:
+To create a custom organizational unit tree:
-. Select one or several org units from the tree on the left.
-. Select a destination org unit in the tree on the right.
-. Click the button _Copy Selected Org Units to Custom Tree_
+. Select one or several organizational units from the tree on the left.
+. Select a destination organizational unit in the tree on the right.
+. Click the button *Copy Selected Org Units to Custom Tree*.
+
.. If you copy a parent/child pair from the tree on the left, this parent/child relationship will persist in the tree on the right.
-. If you click on an org unit on the right-hand tree you will see several icons:
+. If you click on an organizational unit on the right-hand tree, you will see several icons:
+
image::custom_ou_trees/custom_ou_trees_options.png[Icons to move Org Units]
+
-.. Up arrow - this moves the org unit up in the display order, within a set of sibling organizational units. I.e., this action will not move an org unit to a new parent org unit.
-.. Down arrow - this moves the org unit down in the display order, within a set of sibling organizational units. I.e., this action will not move an org unit to a new parent org unit.
-.. Right arrow - this will open a modal _Move Org Unit Elsewhere_
-... Within this modal, select a new parent for the org unit and then click Move Org Unit Here. This will move the org unit and any of its descendants to a new parent org unit.
+.. Up arrow - this moves the organizational unit up in the display order, within a set of sibling organizational units; i.e., this action will not move an organizational unit to a new parent organizational unit.
+.. Down arrow - this moves the organizational unit down in the display order, within a set of sibling organizational units; i.e., this action will not move an organizational unit to a new parent organizational unit.
+.. Right arrow - this will open a modal *Move Org Unit Elsewhere*.
+... Within this modal, select a new parent for the organizational unit and then click *Move Org Unit Here*. This will move the organizational unit and any of its descendants to a new parent organizational unit.
+
image::custom_ou_trees/custom_ou_trees_move.png[Moving an Org Unit]
+
-.. Trashcan - this will delete the selected org unit from the custom org unit tree.
-. Once you have finished making your changes, click _Save Changes_.
+.. Trash can - this will delete the selected organizational unit from the custom organizational unit tree.
+. Once you have finished making your changes, click *Save Changes*.
-When you are ready for the tree to be OPAC-visible, select _Activate Tree_ and then _Save Changes_. You will need to notify your System Administrator to reload Apache in order for your changes to be reflected in the OPAC. Only one Custom Org Unit Tree can be active at a time.
\ No newline at end of file
+When you are ready for the tree to be OPAC-visible, select *Activate Tree* and then *Save Changes*. You will need to notify your System Administrator to reload Apache in order for your changes to be reflected in the OPAC. Only one custom organizational unit tree can be active at a time.
-----------------------------------------------------------------------
Summary of changes:
.../admin_initial_setup/pages/custom_ou_trees.adoc | 34 +++++++++++-----------
1 file changed, 17 insertions(+), 17 deletions(-)
hooks/post-receive
--
Evergreen ILS
1
0

[GIT] Evergreen ILS branch main updated. 7425bd671b68a186c9335c23d9074e39da4016fa
by Git User 17 Apr '25
by Git User 17 Apr '25
17 Apr '25
This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".
The branch, main has been updated
via 7425bd671b68a186c9335c23d9074e39da4016fa (commit)
from fd5bdc1da02a345394a31c0946ed7336671d1b40 (commit)
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
- Log -----------------------------------------------------------------
commit 7425bd671b68a186c9335c23d9074e39da4016fa
Author: Jane Sandberg <js7389(a)princeton.edu>
Date: Thu Apr 17 09:12:10 2025 -0700
npx ng lint --fix
Signed-off-by: Jane Sandberg <js7389(a)princeton.edu>
diff --git a/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts b/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts
index 7dd8cef80e..2ad23d006f 100644
--- a/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts
@@ -60,7 +60,7 @@ export class DialogComponent implements OnInit {
// The modalRef allows direct control of the modal instance.
protected modalRef: NgbModalRef = null;
-
+
public focusable: string;
constructor(
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts
index 5db6e60d7d..7fab6df25f 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts
@@ -147,11 +147,11 @@ describe('CopyAttrsComponent', () => {
// Also assume that we have no item fields
component.batchAttrs = new QueryList();
- //component.saveTemplate(false);
+ // component.saveTemplate(false);
- //expect(component.volcopy.templates[0]).toEqual({callnumber: {prefix: 10}});
+ // expect(component.volcopy.templates[0]).toEqual({callnumber: {prefix: 10}});
- //expect(volCopyServiceMock.saveTemplates).toHaveBeenCalled();
+ // expect(volCopyServiceMock.saveTemplates).toHaveBeenCalled();
});
});
describe('when multiple fields have changed', () => {
@@ -187,7 +187,7 @@ describe('CopyAttrsComponent', () => {
component.saveTemplate(component.volcopy.templates[0]);
// toHash()
- let hashedCallNumber = {
+ const hashedCallNumber = {
ischanged: component.volcopy.templates[0].callnumber.ischanged(),
label_class: component.volcopy.templates[0].callnumber.label_class(),
prefix: component.volcopy.templates[0].callnumber.prefix(),
-----------------------------------------------------------------------
Summary of changes:
Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts | 2 +-
.../eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts | 8 ++++----
2 files changed, 5 insertions(+), 5 deletions(-)
hooks/post-receive
--
Evergreen ILS
1
0

[GIT] Evergreen ILS branch main updated. fd5bdc1da02a345394a31c0946ed7336671d1b40
by Git User 16 Apr '25
by Git User 16 Apr '25
16 Apr '25
This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".
The branch, main has been updated
via fd5bdc1da02a345394a31c0946ed7336671d1b40 (commit)
via b39e987eded3dce5921059d2c910fcdb1930d480 (commit)
via be56cbc0adc5e6ed799d0eecbafc0906fd23ab8a (commit)
from 06fabb27ef20708be766e68284535cf60afd78f2 (commit)
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
- Log -----------------------------------------------------------------
commit fd5bdc1da02a345394a31c0946ed7336671d1b40
Author: Stephanie Leary <stephanie.leary(a)equinoxoli.org>
Date: Fri Apr 4 20:57:07 2025 +0000
LP2106780 Adjust karma.conf.js for concurrency, logging, timeouts
Adjusts the unit test settings:
* Allow concurrent tests
* Silence console logging other than the tests'
* Increase timeout
Signed-off-by: Stephanie Leary <stephanie.leary(a)equinoxoli.org>
Signed-off-by: Jane Sandberg <js7389(a)princeton.edu>
diff --git a/Open-ILS/src/eg2/karma.conf.js b/Open-ILS/src/eg2/karma.conf.js
index a24a7112f1..b6a17546ba 100644
--- a/Open-ILS/src/eg2/karma.conf.js
+++ b/Open-ILS/src/eg2/karma.conf.js
@@ -15,6 +15,7 @@ module.exports = function (config) {
require('@angular-devkit/build-angular/plugins/karma')
],
client:{
+ captureConsole: false,
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
@@ -27,8 +28,12 @@ module.exports = function (config) {
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
- logLevel: config.LOG_INFO,
+ logLevel: config.LOG_ERROR,
autoWatch: true,
+ concurrency: 5,
+ browserNoActivityTimeout: 30000,
+ browserDisconnectTimeout: 30000,
+ pingTimeout: 100000,
browsers: ['ChromeHeadless','FirefoxHeadless'],
customLaunchers: {
'FirefoxHeadless': {
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts
index 4a870d401f..5db6e60d7d 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts
@@ -184,10 +184,17 @@ describe('CopyAttrsComponent', () => {
// Also assume that we have no item fields
component.batchAttrs = new QueryList();
-
component.saveTemplate(component.volcopy.templates[0]);
- expect(component.volcopy.templates[0].callnumber).toEqual(jasmine.objectContaining({
+ // toHash()
+ let hashedCallNumber = {
+ ischanged: component.volcopy.templates[0].callnumber.ischanged(),
+ label_class: component.volcopy.templates[0].callnumber.label_class(),
+ prefix: component.volcopy.templates[0].callnumber.prefix(),
+ suffix: component.volcopy.templates[0].callnumber.suffix()
+ };
+
+ expect(hashedCallNumber).toEqual(jasmine.objectContaining({
ischanged: ['prefix', 'label_class', 'suffix'],
label_class: 1,
prefix: 10,
commit b39e987eded3dce5921059d2c910fcdb1930d480
Author: Stephanie Leary <stephanie.leary(a)equinoxoli.org>
Date: Fri Apr 4 20:56:51 2025 +0000
LP2106780 IDL service, Item attr unit test fixes
Fixes automated tests for the IDL service and the
new item attributes editor.
Signed-off-by: Stephanie Leary <stephanie.leary(a)equinoxoli.org>
Signed-off-by: Jane Sandberg <js7389(a)princeton.edu>
diff --git a/Open-ILS/src/eg2/src/app/core/idl.spec.ts b/Open-ILS/src/eg2/src/app/core/idl.spec.ts
index 2441ceda92..3d44e4b7cd 100644
--- a/Open-ILS/src/eg2/src/app/core/idl.spec.ts
+++ b/Open-ILS/src/eg2/src/app/core/idl.spec.ts
@@ -168,7 +168,7 @@ describe('IdlService', () => {
expect(at_event.template_output()._isfieldmapper).toBe(true);
expect(at_event.template_output().classname).toBe('ateo');
expect(at_event.template_output().id()).toBe(123);
- expect(at_event.template_output().is_error()).toBe(false);
+ expect(at_event.template_output().is_error()).toBe('f');
});
it('should handle flattened object notation', () => {
@@ -205,8 +205,9 @@ describe('IdlService', () => {
it('should throw error for invalid base class', () => {
service.parseIdl();
const hash = { id: 123 };
- expect(() => service.fromHash(hash, 'not_a_class'))
- .toThrow('Invalid or missing base class: not_a_class.');
+ const baseClass = 'not_a_class';
+ expect(() => service.fromHash(hash, baseClass))
+ .toThrow(new Error(`Invalid or missing base class: ${baseClass}`));
});
it('should preserve primitive values', () => {
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts
index 49f3757d9e..4a870d401f 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts
@@ -147,11 +147,11 @@ describe('CopyAttrsComponent', () => {
// Also assume that we have no item fields
component.batchAttrs = new QueryList();
- component.saveTemplate(false);
+ //component.saveTemplate(false);
- expect(component.volcopy.templates[0]).toEqual({callnumber: {prefix: 10}});
+ //expect(component.volcopy.templates[0]).toEqual({callnumber: {prefix: 10}});
- expect(volCopyServiceMock.saveTemplates).toHaveBeenCalled();
+ //expect(volCopyServiceMock.saveTemplates).toHaveBeenCalled();
});
});
describe('when multiple fields have changed', () => {
@@ -166,10 +166,11 @@ describe('CopyAttrsComponent', () => {
// Assume that we've selected a new prefix in the editor
const callNumber = jasmine.createSpyObj<IdlObject>(['ischanged', 'label_class', 'prefix', 'suffix']);
- callNumber.ischanged.and.returnValue(['prefix', 'label_class']);
+ callNumber.ischanged.and.returnValue(['prefix', 'label_class', 'suffix']);
callNumber.label_class.and.returnValue(1);
callNumber.prefix.and.returnValue(10);
callNumber.suffix.and.returnValue(25);
+
const node = new HoldingsTreeNode();
node.target = callNumber;
const contextMock = jasmine.createSpyObj<VolCopyContext>(['volNodes']);
@@ -179,13 +180,22 @@ describe('CopyAttrsComponent', () => {
contextMock.newNotes = [];
component.context = contextMock;
volCopyServiceMock.currentContext = contextMock;
+ component.volcopy.templates[0].callnumber = callNumber;
// Also assume that we have no item fields
component.batchAttrs = new QueryList();
- component.saveTemplate(false);
-
- expect(component.volcopy.templates[0]).toEqual({callnumber: {prefix: 10, classification: 1}});
+ component.saveTemplate(component.volcopy.templates[0]);
+
+ expect(component.volcopy.templates[0].callnumber).toEqual(jasmine.objectContaining({
+ ischanged: ['prefix', 'label_class', 'suffix'],
+ label_class: 1,
+ prefix: 10,
+ suffix: 25
+ }));
+ expect(component.volcopy.templates[0].callnumber).not.toEqual(jasmine.objectContaining({
+ ischanged: false
+ }));
});
});
});
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.ts
index ffde55270b..988b776b98 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.ts
@@ -1357,30 +1357,6 @@ export class CopyAttrsComponent implements OnInit, OnDestroy, AfterViewInit {
console.debug('Building template: set field, value', field, template[field]);
});
- // Volume attributes that are stored in the template.
- // prefix, suffix and Classification
- // Do we actually want to loop through all volumes for this?
- // No, but this is going the way of the dodo with top-level prefix, suffix, and label_class
- /* this.context.volNodes().forEach(volNode => {
- const vol = volNode.target;
- if(vol.ischanged()){ // Something was changed
- template.callnumber = {};
- if(vol.ischanged().includes('prefix')) {
- template.callnumber['prefix'] = vol['prefix']();
- }
- if(vol.ischanged().includes('suffix')) {
- template.callnumber['suffix'] = vol['suffix']();
- }
- if(vol.ischanged().includes('label_class')) {
- template.callnumber['classification'] = vol['label_class']();
- }
- }
- console.log('Template:',template);
- // volNode.target.forEach(field=>{
- // console.log("Saving Volume info to template",field);
- // });
- });*/
-
// alerts, tags, and notes
const newAlerts = this.volcopy.currentContext.newAlerts || [];
console.debug('Building template: found copy alerts',newAlerts);
commit be56cbc0adc5e6ed799d0eecbafc0906fd23ab8a
Author: Jason Etheridge <jason(a)equinoxOLI.org>
Date: Thu Apr 3 17:25:56 2025 -0700
LP2106780 Unit test updates for 3.15
Fixes all test errors up until a timeout error with saveTemplates.
But also need to perpetuate Dialog constructor fix up the call chain to
maintain accessiblity benefits.
Signed-off-by: Jason Etheridge <jason(a)equinoxOLI.org>
Signed-off-by: Stephanie Leary <stephanie.leary(a)equinoxoli.org>
Signed-off-by: Jane Sandberg <js7389(a)princeton.edu>
diff --git a/Open-ILS/src/eg2/src/app/core/idl.spec.ts b/Open-ILS/src/eg2/src/app/core/idl.spec.ts
index eed84948d2..2441ceda92 100644
--- a/Open-ILS/src/eg2/src/app/core/idl.spec.ts
+++ b/Open-ILS/src/eg2/src/app/core/idl.spec.ts
@@ -104,15 +104,13 @@ describe('IdlService', () => {
service.parseIdl();
const hash = {
id: 123,
- name: 'AN ORG',
- active: true
+ name: 'AN ORG'
};
const org = service.fromHash(hash, 'aou');
expect(org._isfieldmapper).toBe(true);
expect(org.classname).toBe('aou');
expect(org.id()).toBe(123);
expect(org.name()).toBe('AN ORG');
- expect(org.active()).toBe(true);
});
it('should maintain data integrity through roundtrip conversion', () => {
@@ -121,13 +119,11 @@ describe('IdlService', () => {
const original = service.create('aou');
original.id(123);
original.name('Test Org');
- original.active(true);
const parent = service.create('aou');
parent.id(456);
parent.name('Parent Org');
- parent.active(false);
- original.parent(parent);
+ original.parent_ou(parent);
// Convert to hash and back
const hash = service.toHash(original);
@@ -136,46 +132,43 @@ describe('IdlService', () => {
// Verify all properties maintained their values
expect(roundtripped.id()).toBe(original.id());
expect(roundtripped.name()).toBe(original.name());
- expect(roundtripped.active()).toBe(original.active());
- expect(roundtripped.parent().id()).toBe(original.parent().id());
- expect(roundtripped.parent().name()).toBe(original.parent().name());
- expect(roundtripped.parent().active()).toBe(original.parent().active());
+ expect(roundtripped.parent_ou().id()).toBe(original.parent_ou().id());
+ expect(roundtripped.parent_ou().name()).toBe(original.parent_ou().name());
// Verify the objects have the same structure
expect(roundtripped._isfieldmapper).toBe(true);
expect(roundtripped.classname).toBe(original.classname);
- expect(roundtripped.parent()._isfieldmapper).toBe(true);
- expect(roundtripped.parent().classname).toBe(original.parent().classname);
+ expect(roundtripped.parent_ou()._isfieldmapper).toBe(true);
+ expect(roundtripped.parent_ou().classname).toBe(original.parent_ou().classname);
});
it('should handle boolean conversion when enabled', () => {
service.parseIdl();
const hash = {
id: 123,
- name: 'AN ORG',
- active: 't', // PostgreSQL-style boolean
- 'parent.active': 'f'
+ region: 'A REGION',
+ name: 'A SMS CARRIER',
+ active: 't',
+ email_gateway: 'opensrf+$number@localhost'
};
- const org = service.fromHash(hash, 'aou', true);
- expect(org.active()).toBe(true);
- expect(org.parent().active()).toBe(false);
+ const carrier = service.fromHash(hash, 'csc', true);
+ expect(carrier.active()).toBe(true);
});
it('should handle nested IDL objects', () => {
service.parseIdl();
const hash = {
id: 456,
- name: 'Child Org',
- parent: {
+ template_output: {
id: 123,
- name: 'Parent Org'
+ is_error: 'f'
}
};
- const org = service.fromHash(hash, 'aou');
- expect(org.parent()._isfieldmapper).toBe(true);
- expect(org.parent().classname).toBe('aou');
- expect(org.parent().id()).toBe(123);
- expect(org.parent().name()).toBe('Parent Org');
+ const at_event = service.fromHash(hash, 'atev');
+ expect(at_event.template_output()._isfieldmapper).toBe(true);
+ expect(at_event.template_output().classname).toBe('ateo');
+ expect(at_event.template_output().id()).toBe(123);
+ expect(at_event.template_output().is_error()).toBe(false);
});
it('should handle flattened object notation', () => {
@@ -183,14 +176,14 @@ describe('IdlService', () => {
const hash = {
id: 456,
name: 'Child Org',
- 'parent.id': 123,
- 'parent.name': 'Parent Org'
+ 'parent_ou.id': 123,
+ 'parent_ou.name': 'Parent Org'
};
const org = service.fromHash(hash, 'aou');
- expect(org.parent()._isfieldmapper).toBe(true);
- expect(org.parent().classname).toBe('aou');
- expect(org.parent().id()).toBe(123);
- expect(org.parent().name()).toBe('Parent Org');
+ expect(org.parent_ou()._isfieldmapper).toBe(true);
+ expect(org.parent_ou().classname).toBe('aou');
+ expect(org.parent_ou().id()).toBe(123);
+ expect(org.parent_ou().name()).toBe('Parent Org');
});
it('should handle arrays of IDL objects', () => {
@@ -213,7 +206,7 @@ describe('IdlService', () => {
service.parseIdl();
const hash = { id: 123 };
expect(() => service.fromHash(hash, 'not_a_class'))
- .toThrow('Invalid or missing base class: not_a_class');
+ .toThrow('Invalid or missing base class: not_a_class.');
});
it('should preserve primitive values', () => {
diff --git a/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts b/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts
index 668b23b56f..7dd8cef80e 100644
--- a/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts
@@ -1,5 +1,5 @@
import {DOCUMENT} from '@angular/common';
-import {Component, Input, OnInit, ViewChild, TemplateRef, EventEmitter, inject, ElementRef} from '@angular/core';
+import {Component, Input, OnInit, ViewChild, TemplateRef, EventEmitter, Inject, ElementRef} from '@angular/core';
import {Observable, Observer} from 'rxjs';
import {NgbModal, NgbModalRef, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
@@ -49,8 +49,6 @@ export class DialogComponent implements OnInit {
identifier: number = DialogComponent.counter++;
returnFocusTo: any;
- private _document = inject(DOCUMENT);
- private _elRef = inject(ElementRef<HTMLElement>);
// Emitted after open() is called on the ngbModal.
// Note when overriding open(), this will not fire unless also
@@ -62,8 +60,14 @@ export class DialogComponent implements OnInit {
// The modalRef allows direct control of the modal instance.
protected modalRef: NgbModalRef = null;
+
+ public focusable: string;
- constructor(private modalService: NgbModal) {}
+ constructor(
+ private modalService: NgbModal,
+ @Inject(DOCUMENT) private _document: any = document,
+ private _elRef: ElementRef<HTMLElement> = null
+ ) {}
// Close all active dialogs
static closeAll() {
@@ -121,7 +125,7 @@ export class DialogComponent implements OnInit {
// Look for the first focusable element in .modal-body.
// Fallbacks are the footer buttons, then (implicitly) the 'X' close button in the dialog header
private _setFocus() {
- if (!this.modalRef) {return;}
+ if (!this.modalRef || !this._document || !this._elRef) {return;}
if (!this._elRef.nativeElement.contains(this._document.activeElement)) {
const dialogEl = this.modalRef['_windowCmptRef'].instance['_elRef'].nativeElement;
const dialogBodySelector = `.modal-body ${this.getFocusable()}`;
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts
index 930faa5a11..49f3757d9e 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts
@@ -18,7 +18,7 @@ describe('CopyAttrsComponent', () => {
const orgMock = jasmine.createSpyObj<OrgService>(['get']);
const authServiceMock = jasmine.createSpyObj<AuthService>(['user']);
const formatServiceMock = jasmine.createSpyObj<FormatService>(['transform']);
- const storeServiceMock = jasmine.createSpyObj<StoreService>(['setLocalItem', 'getLocalItem']);
+ const storeServiceMock = jasmine.createSpyObj<StoreService>(['setLocalItem','getLocalItem']);
const toastServiceMock = jasmine.createSpyObj<ToastService>(['success']);
const volCopyServiceMock = jasmine.createSpyObj<VolCopyService>(['copyStatIsMagic', 'saveTemplates']);
@@ -26,6 +26,25 @@ describe('CopyAttrsComponent', () => {
component = new CopyAttrsComponent(idlMock, orgMock, authServiceMock,
null, null, formatServiceMock, storeServiceMock,
toastServiceMock, volCopyServiceMock);
+ storeServiceMock.getLocalItem.and.returnValue({});
+
+ const contextMock = new VolCopyContext();
+ contextMock.idl = idlMock;
+ contextMock.org = orgMock;
+
+ contextMock.newAlerts = [];
+ contextMock.newTagMaps = [];
+ contextMock.newNotes = [];
+ contextMock.changedAlerts = [];
+ contextMock.changedTagMaps = [];
+ contextMock.changedNotes = [];
+ contextMock.deletedAlerts = [];
+ contextMock.deletedTagMaps = [];
+ contextMock.deletedNotes = [];
+
+ component.context = contextMock;
+ volCopyServiceMock.currentContext = contextMock;
+
component.copyTemplateCbox = jasmine.createSpyObj<ComboboxComponent>(['entries']);
component.copyTemplateCbox.selected = {id: 0};
});
@@ -105,20 +124,25 @@ describe('CopyAttrsComponent', () => {
savedString.current.and.returnValue(Promise.resolve('saved'));
component.savedHoldingsTemplates = savedString;
- // Assume that there already is a template by this name
component.volcopy.templates = {0: {}};
+ component.context.newAlerts = [];
+ component.context.newTagMaps = [];
+ component.context.newNotes = [];
+
+ volCopyServiceMock.currentContext = component.context;
+
// Assume that we've selected a new prefix in the editor
const callNumber = jasmine.createSpyObj<IdlObject>(['ischanged', 'label_class', 'prefix', 'suffix']);
callNumber.ischanged.and.returnValue(['prefix']);
callNumber.label_class.and.returnValue(1);
callNumber.prefix.and.returnValue(10);
callNumber.suffix.and.returnValue(25);
+
const node = new HoldingsTreeNode();
node.target = callNumber;
+
const contextMock = jasmine.createSpyObj<VolCopyContext>(['volNodes']);
- contextMock.volNodes.and.returnValue([node]);
- component.context = contextMock;
// Also assume that we have no item fields
component.batchAttrs = new QueryList();
@@ -126,6 +150,8 @@ describe('CopyAttrsComponent', () => {
component.saveTemplate(false);
expect(component.volcopy.templates[0]).toEqual({callnumber: {prefix: 10}});
+
+ expect(volCopyServiceMock.saveTemplates).toHaveBeenCalled();
});
});
describe('when multiple fields have changed', () => {
@@ -148,7 +174,11 @@ describe('CopyAttrsComponent', () => {
node.target = callNumber;
const contextMock = jasmine.createSpyObj<VolCopyContext>(['volNodes']);
contextMock.volNodes.and.returnValue([node]);
+ contextMock.newAlerts = [];
+ contextMock.newTagMaps = [];
+ contextMock.newNotes = [];
component.context = contextMock;
+ volCopyServiceMock.currentContext = contextMock;
// Also assume that we have no item fields
component.batchAttrs = new QueryList();
-----------------------------------------------------------------------
Summary of changes:
Open-ILS/src/eg2/karma.conf.js | 7 ++-
Open-ILS/src/eg2/src/app/core/idl.spec.ts | 62 +++++++++----------
.../eg2/src/app/share/dialog/dialog.component.ts | 14 +++--
.../staff/cat/volcopy/copy-attrs.component.spec.ts | 69 ++++++++++++++++++----
.../app/staff/cat/volcopy/copy-attrs.component.ts | 24 --------
5 files changed, 101 insertions(+), 75 deletions(-)
hooks/post-receive
--
Evergreen ILS
1
0

[GIT] Evergreen ILS branch rel_3_15 updated. 1f9702a1013f776dad7df3a49829b9070ec54796
by Git User 16 Apr '25
by Git User 16 Apr '25
16 Apr '25
This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".
The branch, rel_3_15 has been updated
via 1f9702a1013f776dad7df3a49829b9070ec54796 (commit)
via 6335bf5dc0d17f9ef9d3e560662fcca752f522de (commit)
via 158cc33a3d8194731011211671ea080d916be307 (commit)
via dad252378048c5f315c1d57ec6ded057e35f0ce8 (commit)
via 59c54d1fa87c5b7a39cc1f21f32f182d5690ccb5 (commit)
from fe360a02e3e110f85717a908324aaebe0437f45a (commit)
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
- Log -----------------------------------------------------------------
commit 1f9702a1013f776dad7df3a49829b9070ec54796
Author: Jane Sandberg <sandbergja(a)gmail.com>
Date: Mon Mar 31 09:05:03 2025 -0700
LP2074112 follow-up: allow angular unit tests to compile
LP2074112 added a ninth parameter to the constructor of this component, but
did not update the test in which the constructor was called. This caused
the angular unit tests to not be able to compile at all.
This work was done in the collaborative code review -- many thanks for
this!
Signed-off-by: Jane Sandberg <sandbergja(a)gmail.com>
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts
index 19354b6afc..5db6e60d7d 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts
@@ -24,7 +24,7 @@ describe('CopyAttrsComponent', () => {
beforeEach(() => {
component = new CopyAttrsComponent(idlMock, orgMock, authServiceMock,
- null, formatServiceMock, storeServiceMock,
+ null, null, formatServiceMock, storeServiceMock,
toastServiceMock, volCopyServiceMock);
storeServiceMock.getLocalItem.and.returnValue({});
commit 6335bf5dc0d17f9ef9d3e560662fcca752f522de
Author: Stephanie Leary <stephanie.leary(a)equinoxoli.org>
Date: Fri Apr 4 20:57:07 2025 +0000
LP2106780 Adjust karma.conf.js for concurrency, logging, timeouts
Adjusts the unit test settings:
* Allow concurrent tests
* Silence console logging other than the tests'
* Increase timeout
Signed-off-by: Stephanie Leary <stephanie.leary(a)equinoxoli.org>
Signed-off-by: Jane Sandberg <js7389(a)princeton.edu>
diff --git a/Open-ILS/src/eg2/karma.conf.js b/Open-ILS/src/eg2/karma.conf.js
index a24a7112f1..b6a17546ba 100644
--- a/Open-ILS/src/eg2/karma.conf.js
+++ b/Open-ILS/src/eg2/karma.conf.js
@@ -15,6 +15,7 @@ module.exports = function (config) {
require('@angular-devkit/build-angular/plugins/karma')
],
client:{
+ captureConsole: false,
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
@@ -27,8 +28,12 @@ module.exports = function (config) {
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
- logLevel: config.LOG_INFO,
+ logLevel: config.LOG_ERROR,
autoWatch: true,
+ concurrency: 5,
+ browserNoActivityTimeout: 30000,
+ browserDisconnectTimeout: 30000,
+ pingTimeout: 100000,
browsers: ['ChromeHeadless','FirefoxHeadless'],
customLaunchers: {
'FirefoxHeadless': {
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts
index 8462c95018..19354b6afc 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts
@@ -184,10 +184,17 @@ describe('CopyAttrsComponent', () => {
// Also assume that we have no item fields
component.batchAttrs = new QueryList();
-
component.saveTemplate(component.volcopy.templates[0]);
- expect(component.volcopy.templates[0].callnumber).toEqual(jasmine.objectContaining({
+ // toHash()
+ let hashedCallNumber = {
+ ischanged: component.volcopy.templates[0].callnumber.ischanged(),
+ label_class: component.volcopy.templates[0].callnumber.label_class(),
+ prefix: component.volcopy.templates[0].callnumber.prefix(),
+ suffix: component.volcopy.templates[0].callnumber.suffix()
+ };
+
+ expect(hashedCallNumber).toEqual(jasmine.objectContaining({
ischanged: ['prefix', 'label_class', 'suffix'],
label_class: 1,
prefix: 10,
commit 158cc33a3d8194731011211671ea080d916be307
Author: Stephanie Leary <stephanie.leary(a)equinoxoli.org>
Date: Fri Apr 4 20:56:51 2025 +0000
LP2106780 IDL service, Item attr unit test fixes
Fixes automated tests for the IDL service and the
new item attributes editor.
Signed-off-by: Stephanie Leary <stephanie.leary(a)equinoxoli.org>
Signed-off-by: Jane Sandberg <js7389(a)princeton.edu>
diff --git a/Open-ILS/src/eg2/src/app/core/idl.spec.ts b/Open-ILS/src/eg2/src/app/core/idl.spec.ts
index 2441ceda92..3d44e4b7cd 100644
--- a/Open-ILS/src/eg2/src/app/core/idl.spec.ts
+++ b/Open-ILS/src/eg2/src/app/core/idl.spec.ts
@@ -168,7 +168,7 @@ describe('IdlService', () => {
expect(at_event.template_output()._isfieldmapper).toBe(true);
expect(at_event.template_output().classname).toBe('ateo');
expect(at_event.template_output().id()).toBe(123);
- expect(at_event.template_output().is_error()).toBe(false);
+ expect(at_event.template_output().is_error()).toBe('f');
});
it('should handle flattened object notation', () => {
@@ -205,8 +205,9 @@ describe('IdlService', () => {
it('should throw error for invalid base class', () => {
service.parseIdl();
const hash = { id: 123 };
- expect(() => service.fromHash(hash, 'not_a_class'))
- .toThrow('Invalid or missing base class: not_a_class.');
+ const baseClass = 'not_a_class';
+ expect(() => service.fromHash(hash, baseClass))
+ .toThrow(new Error(`Invalid or missing base class: ${baseClass}`));
});
it('should preserve primitive values', () => {
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts
index 1abca67256..8462c95018 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts
@@ -147,11 +147,11 @@ describe('CopyAttrsComponent', () => {
// Also assume that we have no item fields
component.batchAttrs = new QueryList();
- component.saveTemplate(false);
+ //component.saveTemplate(false);
- expect(component.volcopy.templates[0]).toEqual({callnumber: {prefix: 10}});
+ //expect(component.volcopy.templates[0]).toEqual({callnumber: {prefix: 10}});
- expect(volCopyServiceMock.saveTemplates).toHaveBeenCalled();
+ //expect(volCopyServiceMock.saveTemplates).toHaveBeenCalled();
});
});
describe('when multiple fields have changed', () => {
@@ -166,10 +166,11 @@ describe('CopyAttrsComponent', () => {
// Assume that we've selected a new prefix in the editor
const callNumber = jasmine.createSpyObj<IdlObject>(['ischanged', 'label_class', 'prefix', 'suffix']);
- callNumber.ischanged.and.returnValue(['prefix', 'label_class']);
+ callNumber.ischanged.and.returnValue(['prefix', 'label_class', 'suffix']);
callNumber.label_class.and.returnValue(1);
callNumber.prefix.and.returnValue(10);
callNumber.suffix.and.returnValue(25);
+
const node = new HoldingsTreeNode();
node.target = callNumber;
const contextMock = jasmine.createSpyObj<VolCopyContext>(['volNodes']);
@@ -179,13 +180,22 @@ describe('CopyAttrsComponent', () => {
contextMock.newNotes = [];
component.context = contextMock;
volCopyServiceMock.currentContext = contextMock;
+ component.volcopy.templates[0].callnumber = callNumber;
// Also assume that we have no item fields
component.batchAttrs = new QueryList();
- component.saveTemplate(false);
-
- expect(component.volcopy.templates[0]).toEqual({callnumber: {prefix: 10, classification: 1}});
+ component.saveTemplate(component.volcopy.templates[0]);
+
+ expect(component.volcopy.templates[0].callnumber).toEqual(jasmine.objectContaining({
+ ischanged: ['prefix', 'label_class', 'suffix'],
+ label_class: 1,
+ prefix: 10,
+ suffix: 25
+ }));
+ expect(component.volcopy.templates[0].callnumber).not.toEqual(jasmine.objectContaining({
+ ischanged: false
+ }));
});
});
});
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.ts
index ffde55270b..988b776b98 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.ts
@@ -1357,30 +1357,6 @@ export class CopyAttrsComponent implements OnInit, OnDestroy, AfterViewInit {
console.debug('Building template: set field, value', field, template[field]);
});
- // Volume attributes that are stored in the template.
- // prefix, suffix and Classification
- // Do we actually want to loop through all volumes for this?
- // No, but this is going the way of the dodo with top-level prefix, suffix, and label_class
- /* this.context.volNodes().forEach(volNode => {
- const vol = volNode.target;
- if(vol.ischanged()){ // Something was changed
- template.callnumber = {};
- if(vol.ischanged().includes('prefix')) {
- template.callnumber['prefix'] = vol['prefix']();
- }
- if(vol.ischanged().includes('suffix')) {
- template.callnumber['suffix'] = vol['suffix']();
- }
- if(vol.ischanged().includes('label_class')) {
- template.callnumber['classification'] = vol['label_class']();
- }
- }
- console.log('Template:',template);
- // volNode.target.forEach(field=>{
- // console.log("Saving Volume info to template",field);
- // });
- });*/
-
// alerts, tags, and notes
const newAlerts = this.volcopy.currentContext.newAlerts || [];
console.debug('Building template: found copy alerts',newAlerts);
commit dad252378048c5f315c1d57ec6ded057e35f0ce8
Author: Jason Etheridge <jason(a)equinoxOLI.org>
Date: Thu Apr 3 17:25:56 2025 -0700
LP2106780 Unit test updates for 3.15
Fixes all test errors up until a timeout error with saveTemplates.
But also need to perpetuate Dialog constructor fix up the call chain to
maintain accessiblity benefits.
Signed-off-by: Jason Etheridge <jason(a)equinoxOLI.org>
Signed-off-by: Stephanie Leary <stephanie.leary(a)equinoxoli.org>
Signed-off-by: Jane Sandberg <js7389(a)princeton.edu>
diff --git a/Open-ILS/src/eg2/src/app/core/idl.spec.ts b/Open-ILS/src/eg2/src/app/core/idl.spec.ts
index eed84948d2..2441ceda92 100644
--- a/Open-ILS/src/eg2/src/app/core/idl.spec.ts
+++ b/Open-ILS/src/eg2/src/app/core/idl.spec.ts
@@ -104,15 +104,13 @@ describe('IdlService', () => {
service.parseIdl();
const hash = {
id: 123,
- name: 'AN ORG',
- active: true
+ name: 'AN ORG'
};
const org = service.fromHash(hash, 'aou');
expect(org._isfieldmapper).toBe(true);
expect(org.classname).toBe('aou');
expect(org.id()).toBe(123);
expect(org.name()).toBe('AN ORG');
- expect(org.active()).toBe(true);
});
it('should maintain data integrity through roundtrip conversion', () => {
@@ -121,13 +119,11 @@ describe('IdlService', () => {
const original = service.create('aou');
original.id(123);
original.name('Test Org');
- original.active(true);
const parent = service.create('aou');
parent.id(456);
parent.name('Parent Org');
- parent.active(false);
- original.parent(parent);
+ original.parent_ou(parent);
// Convert to hash and back
const hash = service.toHash(original);
@@ -136,46 +132,43 @@ describe('IdlService', () => {
// Verify all properties maintained their values
expect(roundtripped.id()).toBe(original.id());
expect(roundtripped.name()).toBe(original.name());
- expect(roundtripped.active()).toBe(original.active());
- expect(roundtripped.parent().id()).toBe(original.parent().id());
- expect(roundtripped.parent().name()).toBe(original.parent().name());
- expect(roundtripped.parent().active()).toBe(original.parent().active());
+ expect(roundtripped.parent_ou().id()).toBe(original.parent_ou().id());
+ expect(roundtripped.parent_ou().name()).toBe(original.parent_ou().name());
// Verify the objects have the same structure
expect(roundtripped._isfieldmapper).toBe(true);
expect(roundtripped.classname).toBe(original.classname);
- expect(roundtripped.parent()._isfieldmapper).toBe(true);
- expect(roundtripped.parent().classname).toBe(original.parent().classname);
+ expect(roundtripped.parent_ou()._isfieldmapper).toBe(true);
+ expect(roundtripped.parent_ou().classname).toBe(original.parent_ou().classname);
});
it('should handle boolean conversion when enabled', () => {
service.parseIdl();
const hash = {
id: 123,
- name: 'AN ORG',
- active: 't', // PostgreSQL-style boolean
- 'parent.active': 'f'
+ region: 'A REGION',
+ name: 'A SMS CARRIER',
+ active: 't',
+ email_gateway: 'opensrf+$number@localhost'
};
- const org = service.fromHash(hash, 'aou', true);
- expect(org.active()).toBe(true);
- expect(org.parent().active()).toBe(false);
+ const carrier = service.fromHash(hash, 'csc', true);
+ expect(carrier.active()).toBe(true);
});
it('should handle nested IDL objects', () => {
service.parseIdl();
const hash = {
id: 456,
- name: 'Child Org',
- parent: {
+ template_output: {
id: 123,
- name: 'Parent Org'
+ is_error: 'f'
}
};
- const org = service.fromHash(hash, 'aou');
- expect(org.parent()._isfieldmapper).toBe(true);
- expect(org.parent().classname).toBe('aou');
- expect(org.parent().id()).toBe(123);
- expect(org.parent().name()).toBe('Parent Org');
+ const at_event = service.fromHash(hash, 'atev');
+ expect(at_event.template_output()._isfieldmapper).toBe(true);
+ expect(at_event.template_output().classname).toBe('ateo');
+ expect(at_event.template_output().id()).toBe(123);
+ expect(at_event.template_output().is_error()).toBe(false);
});
it('should handle flattened object notation', () => {
@@ -183,14 +176,14 @@ describe('IdlService', () => {
const hash = {
id: 456,
name: 'Child Org',
- 'parent.id': 123,
- 'parent.name': 'Parent Org'
+ 'parent_ou.id': 123,
+ 'parent_ou.name': 'Parent Org'
};
const org = service.fromHash(hash, 'aou');
- expect(org.parent()._isfieldmapper).toBe(true);
- expect(org.parent().classname).toBe('aou');
- expect(org.parent().id()).toBe(123);
- expect(org.parent().name()).toBe('Parent Org');
+ expect(org.parent_ou()._isfieldmapper).toBe(true);
+ expect(org.parent_ou().classname).toBe('aou');
+ expect(org.parent_ou().id()).toBe(123);
+ expect(org.parent_ou().name()).toBe('Parent Org');
});
it('should handle arrays of IDL objects', () => {
@@ -213,7 +206,7 @@ describe('IdlService', () => {
service.parseIdl();
const hash = { id: 123 };
expect(() => service.fromHash(hash, 'not_a_class'))
- .toThrow('Invalid or missing base class: not_a_class');
+ .toThrow('Invalid or missing base class: not_a_class.');
});
it('should preserve primitive values', () => {
diff --git a/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts b/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts
index 668b23b56f..7dd8cef80e 100644
--- a/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts
+++ b/Open-ILS/src/eg2/src/app/share/dialog/dialog.component.ts
@@ -1,5 +1,5 @@
import {DOCUMENT} from '@angular/common';
-import {Component, Input, OnInit, ViewChild, TemplateRef, EventEmitter, inject, ElementRef} from '@angular/core';
+import {Component, Input, OnInit, ViewChild, TemplateRef, EventEmitter, Inject, ElementRef} from '@angular/core';
import {Observable, Observer} from 'rxjs';
import {NgbModal, NgbModalRef, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
@@ -49,8 +49,6 @@ export class DialogComponent implements OnInit {
identifier: number = DialogComponent.counter++;
returnFocusTo: any;
- private _document = inject(DOCUMENT);
- private _elRef = inject(ElementRef<HTMLElement>);
// Emitted after open() is called on the ngbModal.
// Note when overriding open(), this will not fire unless also
@@ -62,8 +60,14 @@ export class DialogComponent implements OnInit {
// The modalRef allows direct control of the modal instance.
protected modalRef: NgbModalRef = null;
+
+ public focusable: string;
- constructor(private modalService: NgbModal) {}
+ constructor(
+ private modalService: NgbModal,
+ @Inject(DOCUMENT) private _document: any = document,
+ private _elRef: ElementRef<HTMLElement> = null
+ ) {}
// Close all active dialogs
static closeAll() {
@@ -121,7 +125,7 @@ export class DialogComponent implements OnInit {
// Look for the first focusable element in .modal-body.
// Fallbacks are the footer buttons, then (implicitly) the 'X' close button in the dialog header
private _setFocus() {
- if (!this.modalRef) {return;}
+ if (!this.modalRef || !this._document || !this._elRef) {return;}
if (!this._elRef.nativeElement.contains(this._document.activeElement)) {
const dialogEl = this.modalRef['_windowCmptRef'].instance['_elRef'].nativeElement;
const dialogBodySelector = `.modal-body ${this.getFocusable()}`;
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts
index dc875658cc..1abca67256 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts
@@ -18,7 +18,7 @@ describe('CopyAttrsComponent', () => {
const orgMock = jasmine.createSpyObj<OrgService>(['get']);
const authServiceMock = jasmine.createSpyObj<AuthService>(['user']);
const formatServiceMock = jasmine.createSpyObj<FormatService>(['transform']);
- const storeServiceMock = jasmine.createSpyObj<StoreService>(['setLocalItem', 'getLocalItem']);
+ const storeServiceMock = jasmine.createSpyObj<StoreService>(['setLocalItem','getLocalItem']);
const toastServiceMock = jasmine.createSpyObj<ToastService>(['success']);
const volCopyServiceMock = jasmine.createSpyObj<VolCopyService>(['copyStatIsMagic', 'saveTemplates']);
@@ -26,6 +26,25 @@ describe('CopyAttrsComponent', () => {
component = new CopyAttrsComponent(idlMock, orgMock, authServiceMock,
null, formatServiceMock, storeServiceMock,
toastServiceMock, volCopyServiceMock);
+ storeServiceMock.getLocalItem.and.returnValue({});
+
+ const contextMock = new VolCopyContext();
+ contextMock.idl = idlMock;
+ contextMock.org = orgMock;
+
+ contextMock.newAlerts = [];
+ contextMock.newTagMaps = [];
+ contextMock.newNotes = [];
+ contextMock.changedAlerts = [];
+ contextMock.changedTagMaps = [];
+ contextMock.changedNotes = [];
+ contextMock.deletedAlerts = [];
+ contextMock.deletedTagMaps = [];
+ contextMock.deletedNotes = [];
+
+ component.context = contextMock;
+ volCopyServiceMock.currentContext = contextMock;
+
component.copyTemplateCbox = jasmine.createSpyObj<ComboboxComponent>(['entries']);
component.copyTemplateCbox.selected = {id: 0};
});
@@ -105,20 +124,25 @@ describe('CopyAttrsComponent', () => {
savedString.current.and.returnValue(Promise.resolve('saved'));
component.savedHoldingsTemplates = savedString;
- // Assume that there already is a template by this name
component.volcopy.templates = {0: {}};
+ component.context.newAlerts = [];
+ component.context.newTagMaps = [];
+ component.context.newNotes = [];
+
+ volCopyServiceMock.currentContext = component.context;
+
// Assume that we've selected a new prefix in the editor
const callNumber = jasmine.createSpyObj<IdlObject>(['ischanged', 'label_class', 'prefix', 'suffix']);
callNumber.ischanged.and.returnValue(['prefix']);
callNumber.label_class.and.returnValue(1);
callNumber.prefix.and.returnValue(10);
callNumber.suffix.and.returnValue(25);
+
const node = new HoldingsTreeNode();
node.target = callNumber;
+
const contextMock = jasmine.createSpyObj<VolCopyContext>(['volNodes']);
- contextMock.volNodes.and.returnValue([node]);
- component.context = contextMock;
// Also assume that we have no item fields
component.batchAttrs = new QueryList();
@@ -126,6 +150,8 @@ describe('CopyAttrsComponent', () => {
component.saveTemplate(false);
expect(component.volcopy.templates[0]).toEqual({callnumber: {prefix: 10}});
+
+ expect(volCopyServiceMock.saveTemplates).toHaveBeenCalled();
});
});
describe('when multiple fields have changed', () => {
@@ -148,7 +174,11 @@ describe('CopyAttrsComponent', () => {
node.target = callNumber;
const contextMock = jasmine.createSpyObj<VolCopyContext>(['volNodes']);
contextMock.volNodes.and.returnValue([node]);
+ contextMock.newAlerts = [];
+ contextMock.newTagMaps = [];
+ contextMock.newNotes = [];
component.context = contextMock;
+ volCopyServiceMock.currentContext = contextMock;
// Also assume that we have no item fields
component.batchAttrs = new QueryList();
commit 59c54d1fa87c5b7a39cc1f21f32f182d5690ccb5
Author: Jane Sandberg <js7389(a)princeton.edu>
Date: Thu Apr 3 15:34:42 2025 -0700
LP2074112 follow-up: fix two unit tests
Previously, the StoreService's setLocalItem method was mocked in these
tests. However, the production code now also uses getLocalItem, so
mocking that method gets two failing tests passing.
Signed-off-by: Jane Sandberg <js7389(a)princeton.edu>
diff --git a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts
index 465bb742f6..dc875658cc 100644
--- a/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts
+++ b/Open-ILS/src/eg2/src/app/staff/cat/volcopy/copy-attrs.component.spec.ts
@@ -18,7 +18,7 @@ describe('CopyAttrsComponent', () => {
const orgMock = jasmine.createSpyObj<OrgService>(['get']);
const authServiceMock = jasmine.createSpyObj<AuthService>(['user']);
const formatServiceMock = jasmine.createSpyObj<FormatService>(['transform']);
- const storeServiceMock = jasmine.createSpyObj<StoreService>(['setLocalItem']);
+ const storeServiceMock = jasmine.createSpyObj<StoreService>(['setLocalItem', 'getLocalItem']);
const toastServiceMock = jasmine.createSpyObj<ToastService>(['success']);
const volCopyServiceMock = jasmine.createSpyObj<VolCopyService>(['copyStatIsMagic', 'saveTemplates']);
-----------------------------------------------------------------------
Summary of changes:
Open-ILS/src/eg2/karma.conf.js | 7 ++-
Open-ILS/src/eg2/src/app/core/idl.spec.ts | 62 +++++++++----------
.../eg2/src/app/share/dialog/dialog.component.ts | 14 +++--
.../staff/cat/volcopy/copy-attrs.component.spec.ts | 71 ++++++++++++++++++----
.../app/staff/cat/volcopy/copy-attrs.component.ts | 24 --------
5 files changed, 102 insertions(+), 76 deletions(-)
hooks/post-receive
--
Evergreen ILS
1
0

[GIT] Evergreen ILS branch rel_3_13 updated. 7c994436d777e30c5e73a5476ca0e1fd739b87ac
by Git User 15 Apr '25
by Git User 15 Apr '25
15 Apr '25
This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".
The branch, rel_3_13 has been updated
via 7c994436d777e30c5e73a5476ca0e1fd739b87ac (commit)
from 915223435040e2f70fd30b46e205bf71fbc0f7b4 (commit)
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
- Log -----------------------------------------------------------------
commit 7c994436d777e30c5e73a5476ca0e1fd739b87ac
Author: Michele Morgan <mmorgan(a)noblenet.org>
Date: Tue Apr 15 16:32:29 2025 -0400
Forward-port 3.13.9-3.13.10 upgrade script
Signed-off-by: Michele Morgan <mmorgan(a)noblenet.org>
diff --git a/Open-ILS/src/sql/Pg/version-upgrade/3.13.9-3.13.10-upgrade-db.sql b/Open-ILS/src/sql/Pg/version-upgrade/3.13.9-3.13.10-upgrade-db.sql
new file mode 100644
index 0000000000..3a86a7c8f9
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/version-upgrade/3.13.9-3.13.10-upgrade-db.sql
@@ -0,0 +1,9 @@
+--Upgrade Script for 3.13.9 to 3.13.10
+\set eg_version '''3.13.10'''
+BEGIN;
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('3.13.10', :eg_version);
+COMMIT;
+
+-- Update auditor tables to catch changes to source tables.
+-- Can be removed/skipped if there were no schema changes.
+SELECT auditor.update_auditors();
-----------------------------------------------------------------------
Summary of changes:
.../{3.13.1-3.13.2-upgrade-db.sql => 3.13.9-3.13.10-upgrade-db.sql} | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
copy Open-ILS/src/sql/Pg/version-upgrade/{3.13.1-3.13.2-upgrade-db.sql => 3.13.9-3.13.10-upgrade-db.sql} (72%)
hooks/post-receive
--
Evergreen ILS
1
0

[GIT] Evergreen ILS branch rel_3_14 updated. dfdc605357bcefa2434787d66b29d6b430b875ce
by Git User 15 Apr '25
by Git User 15 Apr '25
15 Apr '25
This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".
The branch, rel_3_14 has been updated
via dfdc605357bcefa2434787d66b29d6b430b875ce (commit)
from d2e68ea4bf745981940b5f68db2d3d98bfd84422 (commit)
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
- Log -----------------------------------------------------------------
commit dfdc605357bcefa2434787d66b29d6b430b875ce
Author: Michele Morgan <mmorgan(a)noblenet.org>
Date: Tue Apr 15 16:39:49 2025 -0400
Forward-port 3.14.4-3.14.5 upgrade script
Signed-off-by: Michele Morgan <mmorgan(a)noblenet.org>
diff --git a/Open-ILS/src/sql/Pg/version-upgrade/3.14.4-3.14.5-upgrade-db.sql b/Open-ILS/src/sql/Pg/version-upgrade/3.14.4-3.14.5-upgrade-db.sql
new file mode 100644
index 0000000000..0671b7e5f0
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/version-upgrade/3.14.4-3.14.5-upgrade-db.sql
@@ -0,0 +1,9 @@
+--Upgrade Script for 3.14.4 to 3.14.5
+\set eg_version '''3.14.5'''
+BEGIN;
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('3.14.5', :eg_version);
+COMMIT;
+
+-- Update auditor tables to catch changes to source tables.
+-- Can be removed/skipped if there were no schema changes.
+SELECT auditor.update_auditors();
-----------------------------------------------------------------------
Summary of changes:
.../{3.12.5-3.12.6-upgrade-db.sql => 3.14.4-3.14.5-upgrade-db.sql} | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
copy Open-ILS/src/sql/Pg/version-upgrade/{3.12.5-3.12.6-upgrade-db.sql => 3.14.4-3.14.5-upgrade-db.sql} (72%)
hooks/post-receive
--
Evergreen ILS
1
0

[GIT] Evergreen ILS branch rel_3_15 updated. fe360a02e3e110f85717a908324aaebe0437f45a
by Git User 15 Apr '25
by Git User 15 Apr '25
15 Apr '25
This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".
The branch, rel_3_15 has been updated
via fe360a02e3e110f85717a908324aaebe0437f45a (commit)
from a829e1d82d73f02330459377030a11075035ef05 (commit)
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
- Log -----------------------------------------------------------------
commit fe360a02e3e110f85717a908324aaebe0437f45a
Author: Michele Morgan <mmorgan(a)noblenet.org>
Date: Tue Apr 15 16:52:22 2025 -0400
Forward-port 3.14.4-3.15.0 upgrade script
Signed-off-by: Michele Morgan <mmorgan(a)noblenet.org>
diff --git a/Open-ILS/src/sql/Pg/version-upgrade/3.14.4-3.15.0-upgrade-db.sql b/Open-ILS/src/sql/Pg/version-upgrade/3.14.4-3.15.0-upgrade-db.sql
new file mode 100644
index 0000000000..2dac5d2ca9
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/version-upgrade/3.14.4-3.15.0-upgrade-db.sql
@@ -0,0 +1,2712 @@
+--Upgrade Script for 3.14.4 to 3.15.0
+\set eg_version '''3.15.0'''
+BEGIN;
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('3.15.0', :eg_version);
+
+SELECT evergreen.upgrade_deps_block_check('1450', :eg_version);
+
+INSERT into config.workstation_setting_type
+ (name, grp, label, description, datatype)
+VALUES (
+ 'eg.admin.keyboard_shortcuts.disable_single',
+ 'gui',
+ oils_i18n_gettext('eg.admin.keyboard_shortcuts.disable_single',
+ 'Staff Client: disable single-letter keyboard shortcuts',
+ 'coust', 'label'),
+ oils_i18n_gettext('eg.admin.keyboard_shortcuts.disable_single',
+ 'Disables single-letter keyboard shortcuts if set to true. Screen reader users should set this to true to avoid interference with standard keyboard shortcuts.',
+ 'coust', 'description'),
+ 'bool'
+);
+
+
+SELECT evergreen.upgrade_deps_block_check('1452', :eg_version);
+
+INSERT INTO config.org_unit_setting_type
+ (name, label, description, grp, datatype)
+VALUES (
+ 'ui.staff.place_holds_for_recent_patrons',
+ oils_i18n_gettext(
+ 'ui.staff.place_holds_for_recent_patrons',
+ 'Place holds for recent patrons',
+ 'coust',
+ 'label'
+ ),
+ oils_i18n_gettext(
+ 'ui.staff.place_holds_for_recent_patrons',
+ 'Loading a patron in the place holds interface designates them as recent. ' ||
+ 'Show the interface to load recent patrons when placing holds.',
+ 'coust',
+ 'description'
+ ),
+ 'gui',
+ 'bool'
+);
+
+
+SELECT evergreen.upgrade_deps_block_check('1453', :eg_version);
+
+INSERT INTO permission.perm_list ( id, code, description ) SELECT DISTINCT
+ 676,
+ 'UPDATE_TOP_OF_QUEUE',
+ oils_i18n_gettext(676,
+ 'Allow setting and unsetting hold from top of hold queue (cut in line)', 'ppl', 'description'
+ )
+ FROM permission.perm_list
+ WHERE NOT EXISTS (SELECT 1 FROM permission.perm_list WHERE code = 'UPDATE_TOP_OF_QUEUE');
+
+
+--Assign permission to any perm groups with UPDATE_HOLD_REQUEST_TIME
+WITH perms_to_add AS
+ (SELECT id FROM
+ permission.perm_list
+ WHERE code IN ('UPDATE_TOP_OF_QUEUE'))
+INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
+ SELECT grp, perms_to_add.id as perm, depth, grantable
+ FROM perms_to_add,
+ permission.grp_perm_map
+
+ --- Don't add the permissions if they have already been assigned
+ WHERE grp NOT IN
+ (SELECT DISTINCT grp FROM permission.grp_perm_map
+ INNER JOIN perms_to_add ON perm=perms_to_add.id)
+
+ --- we're going to match the depth of their existing perm
+ AND perm = (
+ SELECT id
+ FROM permission.perm_list
+ WHERE code = 'UPDATE_HOLD_REQUEST_TIME'
+ );
+
+-- Add missing FROM clause entry to asset.opac_lasso_record_copy_count
+
+
+SELECT evergreen.upgrade_deps_block_check('1455', :eg_version);
+
+CREATE OR REPLACE FUNCTION asset.opac_lasso_record_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
+DECLARE
+ ans RECORD;
+ trans INT;
+BEGIN
+ SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
+
+ FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
+ RETURN QUERY
+ WITH org_list AS (SELECT ARRAY_AGG(id)::BIGINT[] AS orgs FROM actor.org_unit_descendants(ans.id) x),
+ available_statuses AS (SELECT ARRAY_AGG(id) AS ids FROM config.copy_status WHERE is_available),
+ mask AS (SELECT c_attrs FROM asset.patron_default_visibility_mask() x)
+ SELECT -1,
+ ans.id,
+ COUNT( av.id ),
+ SUM( (cp.status = ANY (available_statuses.ids))::INT ),
+ COUNT( av.id ),
+ trans
+ FROM mask,
+ org_list,
+ available_statuses,
+ asset.copy_vis_attr_cache av
+ JOIN asset.copy cp ON (cp.id = av.target_copy AND av.record = rid)
+ WHERE cp.circ_lib = ANY (org_list.orgs) AND av.vis_attr_vector @@ mask.c_attrs::query_int
+ GROUP BY 1,2,6;
+
+ IF NOT FOUND THEN
+ RETURN QUERY SELECT -1, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
+ END IF;
+
+ END LOOP;
+
+ RETURN;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+
+SELECT evergreen.upgrade_deps_block_check('1456', :eg_version);
+
+INSERT into config.workstation_setting_type
+ (name, grp, label, description, datatype)
+VALUES (
+ 'ui.staff.grid.density',
+ 'gui',
+ oils_i18n_gettext('ui.staff.grid.density',
+ 'Grid UI density',
+ 'coust', 'label'),
+ oils_i18n_gettext('ui.staff.grid.density',
+ 'Whitespace around table cells in staff UI data grids. Default is "standard".',
+ 'coust', 'description'),
+ 'string'
+);
+
+
+-- Move OPAC alert banner feature from config file to a library setting
+
+SELECT evergreen.upgrade_deps_block_check('1458', :eg_version);
+
+INSERT into config.org_unit_setting_type
+ (name, grp, label, description, datatype)
+VALUES (
+ 'opac.alert_banner_show',
+ 'opac',
+ oils_i18n_gettext('opac.alert_banner_show',
+ 'OPAC Alert Banner: Display',
+ 'coust', 'label'),
+ oils_i18n_gettext('opac.alert_message_show',
+ 'Show an alert banner in the OPAC. Default is false.',
+ 'coust', 'description'),
+ 'bool'
+);
+
+INSERT into config.org_unit_setting_type
+ (name, grp, label, description, datatype)
+VALUES (
+ 'opac.alert_banner_type',
+ 'opac',
+ oils_i18n_gettext('opac.alert_banner_type',
+ 'OPAC Alert Banner: Type',
+ 'coust', 'label'),
+ oils_i18n_gettext('opac.alert_message_type',
+ 'Determine the display of the banner. Options are: success, info, warning, danger.',
+ 'coust', 'description'),
+ 'string'
+);
+
+INSERT into config.org_unit_setting_type
+ (name, grp, label, description, datatype)
+VALUES (
+ 'opac.alert_banner_text',
+ 'opac',
+ oils_i18n_gettext('opac.alert_banner_text',
+ 'OPAC Alert Banner: Text',
+ 'coust', 'label'),
+ oils_i18n_gettext('opac.alert_message_text',
+ 'Text that will display in the alert banner.',
+ 'coust', 'description'),
+ 'string'
+);
+
+
+-- Move Google Analytics settings from config.tt2 to library settings
+
+SELECT evergreen.upgrade_deps_block_check('1459', :eg_version);
+
+INSERT into config.org_unit_setting_type
+ (name, grp, label, description, datatype)
+VALUES (
+ 'opac.google_analytics_enable',
+ 'opac',
+ oils_i18n_gettext('opac.google_analytics_enable',
+ 'Google Analytics: Enable',
+ 'coust', 'label'),
+ oils_i18n_gettext('opac.alert_message_show',
+ 'Enable Google Analytics in the OPAC. Default is false.',
+ 'coust', 'description'),
+ 'bool'
+);
+
+INSERT into config.org_unit_setting_type
+ (name, grp, label, description, datatype)
+VALUES (
+ 'opac.google_analytics_code',
+ 'opac',
+ oils_i18n_gettext('opac.google_analytics_code',
+ 'Google Analytics: Code',
+ 'coust', 'label'),
+ oils_i18n_gettext('opac.google_analytics_code',
+ 'Account code provided by Google. (Example: G-GVGQ11X12)',
+ 'coust', 'description'),
+ 'string'
+);
+
+
+SELECT evergreen.upgrade_deps_block_check('1460', :eg_version); -- JBoyer
+
+-- Note: the value will not be consistent from system to system. It's an opaque key that has no meaning of its own,
+-- if the value cached in the client does not match or is missing, it clears some cached values and then saves the current value.
+INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
+ 'staff.client_cache_key',
+ oils_i18n_gettext(
+ 'staff.client_cache_key',
+ 'Change this value to force staff clients to clear some cached values',
+ 'cgf',
+ 'label'
+ ),
+ md5(random()::text),
+ TRUE
+);
+
+
+SELECT evergreen.upgrade_deps_block_check('1461', :eg_version);
+
+-- for searching e.g. "111-111-1111"
+CREATE INDEX actor_usr_setting_phone_values_idx
+ ON actor.usr_setting (evergreen.lowercase(value))
+ WHERE name IN ('opac.default_phone', 'opac.default_sms_notify');
+
+-- for searching e.g. "1111111111"
+CREATE INDEX actor_usr_setting_phone_values_numeric_idx
+ ON actor.usr_setting (evergreen.lowercase(REGEXP_REPLACE(value, '[^0-9]', '', 'g')))
+ WHERE name IN ('opac.default_phone', 'opac.default_sms_notify');
+
+
+SELECT evergreen.upgrade_deps_block_check('1462', :eg_version);
+
+INSERT INTO acq.edi_attr (key, label) VALUES
+ ('LINEITEM_SEQUENTIAL_ID',
+ oils_i18n_gettext('LINEITEM_SEQUENTIAL_ID',
+ 'Lineitems Are Enumerated Sequentially', 'aea', 'label'));
+
+
+SELECT evergreen.upgrade_deps_block_check('1463', :eg_version);
+
+INSERT INTO config.org_unit_setting_type
+ (grp, name, datatype, label, description)
+VALUES (
+ 'gui',
+ 'ui.cat.volume_copy_editor.template_bar.show_save_template', 'bool',
+ oils_i18n_gettext(
+ 'ui.cat.volume_copy_editor.template_bar.show_save_template',
+ 'Show "Save Template" in Holdings Editor',
+ 'coust',
+ 'label'
+ ),
+ oils_i18n_gettext(
+ 'ui.cat.volume_copy_editor.template_bar.show_save_template',
+ 'Displays the "Save Template" button for the template bar in the Volume/Copy/Holdings Editor. By default, this is only displayed when working with templates from the Admin interface.',
+ 'coust',
+ 'description'
+ )
+);
+
+INSERT INTO config.org_unit_setting_type
+ (grp, name, datatype, label, description)
+VALUES (
+ 'gui',
+ 'ui.cat.volume_copy_editor.hide_template_bar', 'bool',
+ oils_i18n_gettext(
+ 'ui.cat.volume_copy_editor.hide_template_bar',
+ 'Hide the entire template bar in Holdings Editor',
+ 'coust',
+ 'label'
+ ),
+ oils_i18n_gettext(
+ 'ui.cat.volume_copy_editor.hide_template_bar',
+ 'Hides the template bar in the Volume/Copy/Holdings Editor. By default, the template bar is displayed in this interface.',
+ 'coust',
+ 'description'
+ )
+);
+
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+ 'eg.grid.cat.volcopy.template_grid', 'gui', 'object',
+ oils_i18n_gettext(
+ 'eg.grid.cat.volcopy.template_grid',
+ 'Holdings Template Grid Settings',
+ 'cwst', 'label'
+ )
+);
+
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+ 'eg.grid.holdings.copy_tags.tag_map_list', 'gui', 'object',
+ oils_i18n_gettext(
+ 'eg.grid.holdings.copy_tags.tag_map_list',
+ 'Copy Tag Maps Template Grid Settings',
+ 'cwst', 'label'
+ )
+);
+
+
+SELECT evergreen.upgrade_deps_block_check('1464', :eg_version);
+
+-- If you want to know how many items are available in a particular library group,
+-- you can't easily sum the results of, say, asset.staff_lasso_record_copy_count,
+-- since library groups can theoretically include descendants of other org units
+-- in the library group (for example, the group could include a system and a branch
+-- within that same system), which means that certain items would be counted twice.
+-- The following functions address that problem by providing deduplicated sums that
+-- only count each item once.
+
+CREATE OR REPLACE FUNCTION asset.staff_lasso_record_copy_count_sum(lasso_id INT, record_id BIGINT)
+ RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT, library_group INT) AS $$
+ BEGIN
+ IF (lasso_id IS NULL) THEN RETURN; END IF;
+ IF (record_id IS NULL) THEN RETURN; END IF;
+ RETURN QUERY SELECT
+ -1,
+ -1,
+ COUNT(cp.id),
+ SUM( CASE WHEN cp.status IN (SELECT id FROM config.copy_status WHERE holdable AND is_available) THEN 1 ELSE 0 END ),
+ SUM( CASE WHEN cl.opac_visible AND cp.opac_visible THEN 1 ELSE 0 END),
+ 0,
+ lasso_id
+ FROM ( SELECT DISTINCT descendants.id FROM actor.org_lasso_map aolmp JOIN LATERAL actor.org_unit_descendants(aolmp.org_unit) AS descendants ON TRUE WHERE aolmp.lasso = lasso_id) d
+ JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
+ JOIN asset.copy_location cl ON (cp.location = cl.id AND NOT cl.deleted)
+ JOIN asset.call_number cn ON (cn.record = record_id AND cn.id = cp.call_number AND NOT cn.deleted);
+ END;
+$$ LANGUAGE PLPGSQL STABLE ROWS 1;
+
+CREATE OR REPLACE FUNCTION asset.opac_lasso_record_copy_count_sum(lasso_id INT, record_id BIGINT)
+ RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT, library_group INT) AS $$
+ BEGIN
+ IF (lasso_id IS NULL) THEN RETURN; END IF;
+ IF (record_id IS NULL) THEN RETURN; END IF;
+
+ RETURN QUERY SELECT
+ -1,
+ -1,
+ COUNT(cp.id),
+ SUM( CASE WHEN cp.status IN (SELECT id FROM config.copy_status WHERE holdable AND is_available) THEN 1 ELSE 0 END ),
+ SUM( CASE WHEN cl.opac_visible AND cp.opac_visible THEN 1 ELSE 0 END),
+ 0,
+ lasso_id
+ FROM ( SELECT DISTINCT descendants.id FROM actor.org_lasso_map aolmp JOIN LATERAL actor.org_unit_descendants(aolmp.org_unit) AS descendants ON TRUE WHERE aolmp.lasso = lasso_id) d
+ JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
+ JOIN asset.copy_location cl ON (cp.location = cl.id AND NOT cl.deleted)
+ JOIN asset.call_number cn ON (cn.record = record_id AND cn.id = cp.call_number AND NOT cn.deleted)
+ JOIN asset.copy_vis_attr_cache av ON (cp.id = av.target_copy AND av.record = record_id)
+ JOIN LATERAL (SELECT c_attrs FROM asset.patron_default_visibility_mask()) AS mask ON TRUE
+ WHERE av.vis_attr_vector @@ mask.c_attrs::query_int;
+ END;
+$$ LANGUAGE PLPGSQL STABLE ROWS 1;
+
+CREATE OR REPLACE FUNCTION asset.staff_lasso_metarecord_copy_count_sum(lasso_id INT, metarecord_id BIGINT)
+ RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT, library_group INT) AS $$
+ SELECT (
+ -1,
+ -1,
+ SUM(sums.visible)::bigint,
+ SUM(sums.available)::bigint,
+ SUM(sums.unshadow)::bigint,
+ MIN(sums.transcendant),
+ lasso_id
+ ) FROM metabib.metarecord_source_map mmsm
+ JOIN LATERAL (SELECT visible, available, unshadow, transcendant FROM asset.staff_lasso_record_copy_count_sum(lasso_id, mmsm.source)) sums ON TRUE
+ WHERE mmsm.metarecord = metarecord_id;
+$$ LANGUAGE SQL STABLE ROWS 1;
+
+CREATE OR REPLACE FUNCTION asset.opac_lasso_metarecord_copy_count_sum(lasso_id INT, metarecord_id BIGINT)
+ RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT, library_group INT) AS $$
+ SELECT (
+ -1,
+ -1,
+ SUM(sums.visible)::bigint,
+ SUM(sums.available)::bigint,
+ SUM(sums.unshadow)::bigint,
+ MIN(sums.transcendant),
+ lasso_id
+ ) FROM metabib.metarecord_source_map mmsm
+ JOIN LATERAL (SELECT visible, available, unshadow, transcendant FROM asset.opac_lasso_record_copy_count_sum(lasso_id, mmsm.source)) sums ON TRUE
+ WHERE mmsm.metarecord = metarecord_id;
+$$ LANGUAGE SQL STABLE ROWS 1;
+
+
+CREATE OR REPLACE FUNCTION asset.copy_org_ids(org_units INT[], depth INT, library_groups INT[])
+RETURNS TABLE (id INT)
+AS $$
+DECLARE
+ ancestor INT;
+BEGIN
+ RETURN QUERY SELECT org_unit FROM actor.org_lasso_map WHERE lasso = ANY(library_groups);
+ FOR ancestor IN SELECT unnest(org_units)
+ LOOP
+ RETURN QUERY
+ SELECT d.id
+ FROM actor.org_unit_descendants(ancestor, depth) d;
+ END LOOP;
+ RETURN;
+END;
+$$ LANGUAGE plpgsql;
+
+
+CREATE OR REPLACE FUNCTION asset.staff_copy_total(rec_id INT, org_units INT[], depth INT, library_groups INT[])
+RETURNS INT AS $$
+ SELECT COUNT(cp.id) total
+ FROM asset.copy cp
+ INNER JOIN asset.call_number cn ON (cn.id = cp.call_number AND NOT cn.deleted AND cn.record = rec_id)
+ INNER JOIN asset.copy_location cl ON (cp.location = cl.id AND NOT cl.deleted)
+ WHERE cp.circ_lib = ANY (SELECT asset.copy_org_ids(org_units, depth, library_groups))
+ AND NOT cp.deleted;
+$$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION asset.opac_copy_total(rec_id INT, org_units INT[], depth INT, library_groups INT[])
+RETURNS INT AS $$
+ SELECT COUNT(cp.id) total
+ FROM asset.copy cp
+ INNER JOIN asset.call_number cn ON (cn.id = cp.call_number AND NOT cn.deleted AND cn.record = rec_id)
+ INNER JOIN asset.copy_location cl ON (cp.location = cl.id AND NOT cl.deleted)
+ INNER JOIN asset.copy_vis_attr_cache av ON (cp.id = av.target_copy AND av.record = rec_id)
+ JOIN LATERAL (SELECT c_attrs FROM asset.patron_default_visibility_mask()) AS mask ON TRUE
+ WHERE av.vis_attr_vector @@ mask.c_attrs::query_int
+ AND cp.circ_lib = ANY (SELECT asset.copy_org_ids(org_units, depth, library_groups))
+ AND NOT cp.deleted;
+$$ LANGUAGE SQL;
+
+-- We are adding another argument, which means that we must delete functions with the old signature
+DROP FUNCTION IF EXISTS unapi.holdings_xml(
+ bid BIGINT,
+ ouid INT,
+ org TEXT,
+ depth INT,
+ includes TEXT[],
+ slimit HSTORE,
+ soffset HSTORE,
+ include_xmlns BOOL,
+ pref_lib INT);
+
+CREATE OR REPLACE FUNCTION unapi.holdings_xml (
+ bid BIGINT,
+ ouid INT,
+ org TEXT,
+ depth INT DEFAULT NULL,
+ includes TEXT[] DEFAULT NULL::TEXT[],
+ slimit HSTORE DEFAULT NULL,
+ soffset HSTORE DEFAULT NULL,
+ include_xmlns BOOL DEFAULT TRUE,
+ pref_lib INT DEFAULT NULL,
+ library_group INT DEFAULT NULL
+)
+RETURNS XML AS $F$
+ SELECT XMLELEMENT(
+ name holdings,
+ XMLATTRIBUTES(
+ CASE WHEN $8 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+ CASE WHEN ('bre' = ANY ($5)) THEN 'tag:open-ils.org:U2@bre/' || $1 || '/' || $3 ELSE NULL END AS id,
+ (SELECT record_has_holdable_copy FROM asset.record_has_holdable_copy($1)) AS has_holdable
+ ),
+ XMLELEMENT(
+ name counts,
+ (SELECT XMLAGG(XMLELEMENT::XML) FROM (
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('public' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.opac_ou_record_copy_count($2, $1)
+ UNION
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('staff' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.staff_ou_record_copy_count($2, $1)
+ UNION
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('pref_lib' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.opac_ou_record_copy_count($9, $1)
+ UNION
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('public' as type, depth, library_group, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.opac_lasso_record_copy_count_sum(library_group, $1)
+ UNION
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('staff' as type, depth, library_group, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.staff_lasso_record_copy_count_sum(library_group, $1)
+ ORDER BY 1
+ )x)
+ ),
+ CASE
+ WHEN ('bmp' = ANY ($5)) THEN
+ XMLELEMENT(
+ name monograph_parts,
+ (SELECT XMLAGG(bmp) FROM (
+ SELECT unapi.bmp( id, 'xml', 'monograph_part', array_remove( array_remove($5,'bre'), 'holdings_xml'), $3, $4, $6, $7, FALSE)
+ FROM biblio.monograph_part
+ WHERE NOT deleted AND record = $1
+ )x)
+ )
+ ELSE NULL
+ END,
+ XMLELEMENT(
+ name volumes,
+ (SELECT XMLAGG(acn ORDER BY rank, name, label_sortkey) FROM (
+ -- Physical copies
+ SELECT unapi.acn(y.id,'xml','volume',array_remove( array_remove($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE), y.rank, name, label_sortkey
+ FROM evergreen.ranked_volumes($1, $2, $4, $6, $7, $9, $5) AS y
+ UNION ALL
+ -- Located URIs
+ SELECT unapi.acn(uris.id,'xml','volume',array_remove( array_remove($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE), uris.rank, name, label_sortkey
+ FROM evergreen.located_uris($1, $2, $9) AS uris
+ )x)
+ ),
+ CASE WHEN ('ssub' = ANY ($5)) THEN
+ XMLELEMENT(
+ name subscriptions,
+ (SELECT XMLAGG(ssub) FROM (
+ SELECT unapi.ssub(id,'xml','subscription','{}'::TEXT[], $3, $4, $6, $7, FALSE)
+ FROM serial.subscription
+ WHERE record_entry = $1
+ )x)
+ )
+ ELSE NULL END,
+ CASE WHEN ('acp' = ANY ($5)) THEN
+ XMLELEMENT(
+ name foreign_copies,
+ (SELECT XMLAGG(acp) FROM (
+ SELECT unapi.acp(p.target_copy,'xml','copy',array_remove($5,'acp'), $3, $4, $6, $7, FALSE)
+ FROM biblio.peer_bib_copy_map p
+ JOIN asset.copy c ON (p.target_copy = c.id)
+ WHERE NOT c.deleted AND p.peer_record = $1
+ LIMIT ($6 -> 'acp')::INT
+ OFFSET ($7 -> 'acp')::INT
+ )x)
+ )
+ ELSE NULL END
+ );
+$F$ LANGUAGE SQL STABLE;
+
+DROP FUNCTION IF EXISTS unapi.bre (
+ obj_id BIGINT,
+ format TEXT,
+ ename TEXT,
+ includes TEXT[],
+ org TEXT,
+ depth INT,
+ slimit HSTORE,
+ soffset HSTORE,
+ include_xmlns BOOL,
+ pref_lib INT);
+
+CREATE OR REPLACE FUNCTION unapi.bre (
+ obj_id BIGINT,
+ format TEXT,
+ ename TEXT,
+ includes TEXT[],
+ org TEXT,
+ depth INT DEFAULT NULL,
+ slimit HSTORE DEFAULT NULL,
+ soffset HSTORE DEFAULT NULL,
+ include_xmlns BOOL DEFAULT TRUE,
+ pref_lib INT DEFAULT NULL,
+ library_group INT DEFAULT NULL
+)
+RETURNS XML AS $F$
+DECLARE
+ me biblio.record_entry%ROWTYPE;
+ layout unapi.bre_output_layout%ROWTYPE;
+ xfrm config.xml_transform%ROWTYPE;
+ ouid INT;
+ tmp_xml TEXT;
+ top_el TEXT;
+ output XML;
+ hxml XML;
+ axml XML;
+ source XML;
+BEGIN
+
+ IF org = '-' OR org IS NULL THEN
+ SELECT shortname INTO org FROM evergreen.org_top();
+ END IF;
+
+ SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
+
+ IF ouid IS NULL THEN
+ RETURN NULL::XML;
+ END IF;
+
+ IF format = 'holdings_xml' THEN -- the special case
+ output := unapi.holdings_xml( obj_id, ouid, org, depth, includes, slimit, soffset, include_xmlns, NULL, library_group);
+ RETURN output;
+ END IF;
+
+ SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
+
+ IF layout.name IS NULL THEN
+ RETURN NULL::XML;
+ END IF;
+
+ SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform;
+
+ SELECT * INTO me FROM biblio.record_entry WHERE id = obj_id;
+
+ -- grab bib_source, if any
+ IF ('cbs' = ANY (includes) AND me.source IS NOT NULL) THEN
+ source := unapi.cbs(me.source,NULL,NULL,NULL,NULL);
+ ELSE
+ source := NULL::XML;
+ END IF;
+
+ -- grab SVF if we need them
+ IF ('mra' = ANY (includes)) THEN
+ axml := unapi.mra(obj_id,NULL,NULL,NULL,NULL);
+ ELSE
+ axml := NULL::XML;
+ END IF;
+
+ -- grab holdings if we need them
+ IF ('holdings_xml' = ANY (includes)) THEN
+ hxml := unapi.holdings_xml(obj_id, ouid, org, depth, array_remove(includes,'holdings_xml'), slimit, soffset, include_xmlns, pref_lib, library_group);
+ ELSE
+ hxml := NULL::XML;
+ END IF;
+
+
+ -- generate our item node
+
+
+ IF format = 'marcxml' THEN
+ tmp_xml := me.marc;
+ IF tmp_xml !~ E'<marc:' THEN -- If we're not using the prefixed namespace in this record, then remove all declarations of it
+ tmp_xml := REGEXP_REPLACE(tmp_xml, ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g');
+ END IF;
+ ELSE
+ tmp_xml := oils_xslt_process(me.marc, xfrm.xslt)::XML;
+ END IF;
+
+ top_el := REGEXP_REPLACE(tmp_xml, E'^.*?<((?:\\S+:)?' || layout.holdings_element || ').*$', E'\\1');
+
+ IF source IS NOT NULL THEN
+ tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', source || '</' || top_el || E'>\\1');
+ END IF;
+
+ IF axml IS NOT NULL THEN
+ tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', axml || '</' || top_el || E'>\\1');
+ END IF;
+
+ IF hxml IS NOT NULL THEN -- XXX how do we configure the holdings position?
+ tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1');
+ END IF;
+
+ IF ('bre.unapi' = ANY (includes)) THEN
+ output := REGEXP_REPLACE(
+ tmp_xml,
+ '</' || top_el || '>(.*?)',
+ XMLELEMENT(
+ name abbr,
+ XMLATTRIBUTES(
+ 'http://www.w3.org/1999/xhtml' AS xmlns,
+ 'unapi-id' AS class,
+ 'tag:open-ils.org:U2@bre/' || obj_id || '/' || org AS title
+ )
+ )::TEXT || '</' || top_el || E'>\\1'
+ );
+ ELSE
+ output := tmp_xml;
+ END IF;
+
+ IF ('bre.extern' = ANY (includes)) THEN
+ output := REGEXP_REPLACE(
+ tmp_xml,
+ '</' || top_el || '>(.*?)',
+ XMLELEMENT(
+ name extern,
+ XMLATTRIBUTES(
+ 'http://open-ils.org/spec/biblio/v1' AS xmlns,
+ me.creator AS creator,
+ me.editor AS editor,
+ me.create_date AS create_date,
+ me.edit_date AS edit_date,
+ me.quality AS quality,
+ me.fingerprint AS fingerprint,
+ me.tcn_source AS tcn_source,
+ me.tcn_value AS tcn_value,
+ me.owner AS owner,
+ me.share_depth AS share_depth,
+ me.active AS active,
+ me.deleted AS deleted
+ )
+ )::TEXT || '</' || top_el || E'>\\1'
+ );
+ ELSE
+ output := tmp_xml;
+ END IF;
+
+ output := REGEXP_REPLACE(output::TEXT,E'>\\s+<','><','gs')::XML;
+ RETURN output;
+END;
+$F$ LANGUAGE PLPGSQL STABLE;
+
+DROP FUNCTION IF EXISTS unapi.biblio_record_entry_feed (
+ id_list BIGINT[],
+ format TEXT,
+ includes TEXT[],
+ org TEXT,
+ depth INT,
+ slimit HSTORE,
+ soffset HSTORE,
+ include_xmlns BOOL,
+ title TEXT,
+ description TEXT,
+ creator TEXT,
+ update_ts TEXT,
+ unapi_url TEXT,
+ header_xml XML,
+ pref_lib INT);
+CREATE OR REPLACE FUNCTION unapi.biblio_record_entry_feed (
+ id_list BIGINT[],
+ format TEXT,
+ includes TEXT[],
+ org TEXT,
+ depth INT DEFAULT NULL,
+ slimit HSTORE DEFAULT NULL,
+ soffset HSTORE DEFAULT NULL,
+ include_xmlns BOOL DEFAULT TRUE,
+ title TEXT DEFAULT NULL,
+ description TEXT DEFAULT NULL,
+ creator TEXT DEFAULT NULL,
+ update_ts TEXT DEFAULT NULL,
+ unapi_url TEXT DEFAULT NULL,
+ header_xml XML DEFAULT NULL,
+ pref_lib INT DEFAULT NULL,
+ library_group INT DEFAULT NULL
+ ) RETURNS XML AS $F$
+DECLARE
+ layout unapi.bre_output_layout%ROWTYPE;
+ transform config.xml_transform%ROWTYPE;
+ item_format TEXT;
+ tmp_xml TEXT;
+ xmlns_uri TEXT := 'http://open-ils.org/spec/feed-xml/v1';
+ ouid INT;
+ element_list TEXT[];
+BEGIN
+
+ IF org = '-' OR org IS NULL THEN
+ SELECT shortname INTO org FROM evergreen.org_top();
+ END IF;
+
+ SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
+ SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
+
+ IF layout.name IS NULL THEN
+ RETURN NULL::XML;
+ END IF;
+
+ SELECT * INTO transform FROM config.xml_transform WHERE name = layout.transform;
+ xmlns_uri := COALESCE(transform.namespace_uri,xmlns_uri);
+
+ -- Gather the bib xml
+ SELECT XMLAGG( unapi.bre(i, format, '', includes, org, depth, slimit, soffset, include_xmlns, pref_lib, library_group)) INTO tmp_xml FROM UNNEST( id_list ) i;
+
+ IF layout.title_element IS NOT NULL THEN
+ EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.title_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, title;
+ END IF;
+
+ IF layout.description_element IS NOT NULL THEN
+ EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.description_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, description;
+ END IF;
+
+ IF layout.creator_element IS NOT NULL THEN
+ EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.creator_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, creator;
+ END IF;
+
+ IF layout.update_ts_element IS NOT NULL THEN
+ EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.update_ts_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, update_ts;
+ END IF;
+
+ IF unapi_url IS NOT NULL THEN
+ EXECUTE $$SELECT XMLCONCAT( XMLELEMENT( name link, XMLATTRIBUTES( 'http://www.w3.org/1999/xhtml' AS xmlns, 'unapi-server' AS rel, $1 AS href, 'unapi' AS title)), $2)$$ INTO tmp_xml USING unapi_url, tmp_xml::XML;
+ END IF;
+
+ IF header_xml IS NOT NULL THEN tmp_xml := XMLCONCAT(header_xml,tmp_xml::XML); END IF;
+
+ element_list := regexp_split_to_array(layout.feed_top,E'\\.');
+ FOR i IN REVERSE ARRAY_UPPER(element_list, 1) .. 1 LOOP
+ EXECUTE 'SELECT XMLELEMENT( name '|| quote_ident(element_list[i]) ||', XMLATTRIBUTES( $1 AS xmlns), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML;
+ END LOOP;
+
+ RETURN tmp_xml::XML;
+END;
+$F$ LANGUAGE PLPGSQL STABLE;
+
+
+DROP FUNCTION IF EXISTS unapi.metabib_virtual_record_feed (
+ id_list BIGINT[],
+ format TEXT,
+ includes TEXT[],
+ org TEXT,
+ depth INT,
+ slimit HSTORE,
+ soffset HSTORE,
+ include_xmlns BOOL, title TEXT,
+ description TEXT,
+ creator TEXT,
+ update_ts TEXT,
+ unapi_url TEXT,
+ header_xml XML,
+ pref_lib INT);
+CREATE OR REPLACE FUNCTION unapi.metabib_virtual_record_feed (
+ id_list BIGINT[],
+ format TEXT,
+ includes TEXT[],
+ org TEXT,
+ depth INT DEFAULT NULL,
+ slimit HSTORE DEFAULT NULL,
+ soffset HSTORE DEFAULT NULL,
+ include_xmlns BOOL DEFAULT TRUE,
+ title TEXT DEFAULT NULL,
+ description TEXT DEFAULT NULL,
+ creator TEXT DEFAULT NULL,
+ update_ts TEXT DEFAULT NULL,
+ unapi_url TEXT DEFAULT NULL,
+ header_xml XML DEFAULT NULL,
+ pref_lib INT DEFAULT NULL,
+ library_group INT DEFAULT NULL ) RETURNS XML AS $F$
+DECLARE
+ layout unapi.bre_output_layout%ROWTYPE;
+ transform config.xml_transform%ROWTYPE;
+ item_format TEXT;
+ tmp_xml TEXT;
+ xmlns_uri TEXT := 'http://open-ils.org/spec/feed-xml/v1';
+ ouid INT;
+ element_list TEXT[];
+BEGIN
+
+ IF org = '-' OR org IS NULL THEN
+ SELECT shortname INTO org FROM evergreen.org_top();
+ END IF;
+
+ SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
+ SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
+
+ IF layout.name IS NULL THEN
+ RETURN NULL::XML;
+ END IF;
+
+ SELECT * INTO transform FROM config.xml_transform WHERE name = layout.transform;
+ xmlns_uri := COALESCE(transform.namespace_uri,xmlns_uri);
+
+ -- Gather the bib xml
+ SELECT XMLAGG( unapi.mmr(i, format, '', includes, org, depth, slimit, soffset, include_xmlns, pref_lib, library_group)) INTO tmp_xml FROM UNNEST( id_list ) i;
+
+ IF layout.title_element IS NOT NULL THEN
+ EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.title_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, title;
+ END IF;
+
+ IF layout.description_element IS NOT NULL THEN
+ EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.description_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, description;
+ END IF;
+
+ IF layout.creator_element IS NOT NULL THEN
+ EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.creator_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, creator;
+ END IF;
+
+ IF layout.update_ts_element IS NOT NULL THEN
+ EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.update_ts_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, update_ts;
+ END IF;
+
+ IF unapi_url IS NOT NULL THEN
+ EXECUTE $$SELECT XMLCONCAT( XMLELEMENT( name link, XMLATTRIBUTES( 'http://www.w3.org/1999/xhtml' AS xmlns, 'unapi-server' AS rel, $1 AS href, 'unapi' AS title)), $2)$$ INTO tmp_xml USING unapi_url, tmp_xml::XML;
+ END IF;
+
+ IF header_xml IS NOT NULL THEN tmp_xml := XMLCONCAT(header_xml,tmp_xml::XML); END IF;
+
+ element_list := regexp_split_to_array(layout.feed_top,E'\\.');
+ FOR i IN REVERSE ARRAY_UPPER(element_list, 1) .. 1 LOOP
+ EXECUTE 'SELECT XMLELEMENT( name '|| quote_ident(element_list[i]) ||', XMLATTRIBUTES( $1 AS xmlns), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML;
+ END LOOP;
+
+ RETURN tmp_xml::XML;
+END;
+$F$ LANGUAGE PLPGSQL STABLE;
+
+
+DROP FUNCTION IF EXISTS unapi.mmr(
+ obj_id BIGINT,
+ format TEXT,
+ ename TEXT,
+ includes TEXT[],
+ org TEXT,
+ depth INT,
+ slimit HSTORE,
+ soffset HSTORE,
+ include_xmlns BOOL,
+ pref_lib INT
+);
+
+CREATE OR REPLACE FUNCTION unapi.mmr (
+ obj_id BIGINT,
+ format TEXT,
+ ename TEXT,
+ includes TEXT[],
+ org TEXT,
+ depth INT DEFAULT NULL,
+ slimit HSTORE DEFAULT NULL,
+ soffset HSTORE DEFAULT NULL,
+ include_xmlns BOOL DEFAULT TRUE,
+ pref_lib INT DEFAULT NULL,
+ library_group INT DEFAULT NULL
+)
+RETURNS XML AS $F$
+DECLARE
+ mmrec metabib.metarecord%ROWTYPE;
+ leadrec biblio.record_entry%ROWTYPE;
+ subrec biblio.record_entry%ROWTYPE;
+ layout unapi.bre_output_layout%ROWTYPE;
+ xfrm config.xml_transform%ROWTYPE;
+ ouid INT;
+ xml_buf TEXT; -- growing XML document
+ tmp_xml TEXT; -- single-use XML string
+ xml_frag TEXT; -- single-use XML fragment
+ top_el TEXT;
+ output XML;
+ hxml XML;
+ axml XML;
+ subxml XML; -- subordinate records elements
+ sub_xpath TEXT;
+ parts TEXT[];
+BEGIN
+
+ -- xpath for extracting bre.marc values from subordinate records
+ -- so they may be appended to the MARC of the master record prior
+ -- to XSLT processing.
+ -- subjects, isbn, issn, upc -- anything else?
+ sub_xpath :=
+ '//*[starts-with(@tag, "6") or @tag="020" or @tag="022" or @tag="024"]';
+
+ IF org = '-' OR org IS NULL THEN
+ SELECT shortname INTO org FROM evergreen.org_top();
+ END IF;
+
+ SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
+
+ IF ouid IS NULL THEN
+ RETURN NULL::XML;
+ END IF;
+
+ SELECT INTO mmrec * FROM metabib.metarecord WHERE id = obj_id;
+ IF NOT FOUND THEN
+ RETURN NULL::XML;
+ END IF;
+
+ -- TODO: aggregate holdings from constituent records
+ IF format = 'holdings_xml' THEN -- the special case
+ output := unapi.mmr_holdings_xml(
+ obj_id, ouid, org, depth,
+ array_remove(includes,'holdings_xml'),
+ slimit, soffset, include_xmlns, pref_lib, library_group);
+ RETURN output;
+ END IF;
+
+ SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
+
+ IF layout.name IS NULL THEN
+ RETURN NULL::XML;
+ END IF;
+
+ SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform;
+
+ SELECT INTO leadrec * FROM biblio.record_entry WHERE id = mmrec.master_record;
+
+ -- Grab distinct MVF for all records if requested
+ IF ('mra' = ANY (includes)) THEN
+ axml := unapi.mmr_mra(obj_id,NULL,NULL,NULL,org,depth,NULL,NULL,TRUE,pref_lib);
+ ELSE
+ axml := NULL::XML;
+ END IF;
+
+ xml_buf = leadrec.marc;
+
+ hxml := NULL::XML;
+ IF ('holdings_xml' = ANY (includes)) THEN
+ hxml := unapi.mmr_holdings_xml(
+ obj_id, ouid, org, depth,
+ array_remove(includes,'holdings_xml'),
+ slimit, soffset, include_xmlns, pref_lib, library_group);
+ END IF;
+
+ subxml := NULL::XML;
+ parts := '{}'::TEXT[];
+ FOR subrec IN SELECT bre.* FROM biblio.record_entry bre
+ JOIN metabib.metarecord_source_map mmsm ON (mmsm.source = bre.id)
+ JOIN metabib.metarecord mmr ON (mmr.id = mmsm.metarecord)
+ WHERE mmr.id = obj_id AND NOT bre.deleted
+ ORDER BY CASE WHEN bre.id = mmr.master_record THEN 0 ELSE bre.id END
+ LIMIT COALESCE((slimit->'bre')::INT, 5) LOOP
+
+ IF subrec.id = leadrec.id THEN CONTINUE; END IF;
+ -- Append choice data from the the non-lead records to the
+ -- the lead record document
+
+ parts := parts || xpath(sub_xpath, subrec.marc::XML)::TEXT[];
+ END LOOP;
+
+ SELECT STRING_AGG( DISTINCT p , '' )::XML INTO subxml FROM UNNEST(parts) p;
+
+ -- append data from the subordinate records to the
+ -- main record document before applying the XSLT
+
+ IF subxml IS NOT NULL THEN
+ xml_buf := REGEXP_REPLACE(xml_buf,
+ '</record>(.*?)$', subxml || '</record>' || E'\\1');
+ END IF;
+
+ IF format = 'marcxml' THEN
+ -- If we're not using the prefixed namespace in
+ -- this record, then remove all declarations of it
+ IF xml_buf !~ E'<marc:' THEN
+ xml_buf := REGEXP_REPLACE(xml_buf,
+ ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g');
+ END IF;
+ ELSE
+ xml_buf := oils_xslt_process(xml_buf, xfrm.xslt)::XML;
+ END IF;
+
+ -- update top_el to reflect the change in xml_buf, which may
+ -- now be a different type of document (e.g. record -> mods)
+ top_el := REGEXP_REPLACE(xml_buf, E'^.*?<((?:\\S+:)?' ||
+ layout.holdings_element || ').*$', E'\\1');
+
+ IF axml IS NOT NULL THEN
+ xml_buf := REGEXP_REPLACE(xml_buf,
+ '</' || top_el || '>(.*?)$', axml || '</' || top_el || E'>\\1');
+ END IF;
+
+ IF hxml IS NOT NULL THEN
+ xml_buf := REGEXP_REPLACE(xml_buf,
+ '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1');
+ END IF;
+
+ IF ('mmr.unapi' = ANY (includes)) THEN
+ output := REGEXP_REPLACE(
+ xml_buf,
+ '</' || top_el || '>(.*?)',
+ XMLELEMENT(
+ name abbr,
+ XMLATTRIBUTES(
+ 'http://www.w3.org/1999/xhtml' AS xmlns,
+ 'unapi-id' AS class,
+ 'tag:open-ils.org:U2@mmr/' || obj_id || '/' || org AS title
+ )
+ )::TEXT || '</' || top_el || E'>\\1'
+ );
+ ELSE
+ output := xml_buf;
+ END IF;
+
+ -- remove ignorable whitesace
+ output := REGEXP_REPLACE(output::TEXT,E'>\\s+<','><','gs')::XML;
+ RETURN output;
+END;
+$F$ LANGUAGE PLPGSQL STABLE;
+
+DROP FUNCTION IF EXISTS unapi.mmr_holdings_xml (
+ mid BIGINT,
+ ouid INT,
+ org TEXT,
+ depth INT,
+ includes TEXT[],
+ slimit HSTORE,
+ soffset HSTORE,
+ include_xmlns BOOL,
+ pref_lib INT
+);
+
+CREATE OR REPLACE FUNCTION unapi.mmr_holdings_xml (
+ mid BIGINT,
+ ouid INT,
+ org TEXT,
+ depth INT DEFAULT NULL,
+ includes TEXT[] DEFAULT NULL::TEXT[],
+ slimit HSTORE DEFAULT NULL,
+ soffset HSTORE DEFAULT NULL,
+ include_xmlns BOOL DEFAULT TRUE,
+ pref_lib INT DEFAULT NULL,
+ library_group INT DEFAULT NULL
+)
+RETURNS XML AS $F$
+ SELECT XMLELEMENT(
+ name holdings,
+ XMLATTRIBUTES(
+ CASE WHEN $8 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+ CASE WHEN ('mmr' = ANY ($5)) THEN 'tag:open-ils.org:U2@mmr/' || $1 || '/' || $3 ELSE NULL END AS id,
+ (SELECT metarecord_has_holdable_copy FROM asset.metarecord_has_holdable_copy($1)) AS has_holdable
+ ),
+ XMLELEMENT(
+ name counts,
+ (SELECT XMLAGG(XMLELEMENT::XML) FROM (
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('public' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.opac_ou_metarecord_copy_count($2, $1)
+ UNION
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('staff' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.staff_ou_metarecord_copy_count($2, $1)
+ UNION
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('pref_lib' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.opac_ou_metarecord_copy_count($9, $1)
+ UNION
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('public' as type, depth, library_group, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.opac_lasso_metarecord_copy_count_sum(library_group, $1)
+ UNION
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('staff' as type, depth, library_group, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.staff_lasso_metarecord_copy_count_sum(library_group, $1)
+ ORDER BY 1
+ )x)
+ ),
+ -- XXX monograph_parts and foreign_copies are skipped in MRs ... put them back some day?
+ XMLELEMENT(
+ name volumes,
+ (SELECT XMLAGG(acn ORDER BY rank, name, label_sortkey) FROM (
+ -- Physical copies
+ SELECT unapi.acn(y.id,'xml','volume',array_remove( array_remove($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE), y.rank, name, label_sortkey
+ FROM evergreen.ranked_volumes((SELECT ARRAY_AGG(source) FROM metabib.metarecord_source_map WHERE metarecord = $1), $2, $4, $6, $7, $9, $5) AS y
+ UNION ALL
+ -- Located URIs
+ SELECT unapi.acn(uris.id,'xml','volume',array_remove( array_remove($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE), uris.rank, name, label_sortkey
+ FROM evergreen.located_uris((SELECT ARRAY_AGG(source) FROM metabib.metarecord_source_map WHERE metarecord = $1), $2, $9) AS uris
+ )x)
+ ),
+ CASE WHEN ('ssub' = ANY ($5)) THEN
+ XMLELEMENT(
+ name subscriptions,
+ (SELECT XMLAGG(ssub) FROM (
+ SELECT unapi.ssub(id,'xml','subscription','{}'::TEXT[], $3, $4, $6, $7, FALSE)
+ FROM serial.subscription
+ WHERE record_entry IN (SELECT source FROM metabib.metarecord_source_map WHERE metarecord = $1)
+ )x)
+ )
+ ELSE NULL END
+ );
+$F$ LANGUAGE SQL STABLE;
+
+
+SELECT evergreen.upgrade_deps_block_check('1465', :eg_version);
+
+ALTER TABLE config.ui_staff_portal_page_entry
+ADD COLUMN url_newtab boolean;
+
+
+SELECT evergreen.upgrade_deps_block_check('1466', :eg_version);
+
+INSERT into config.workstation_setting_type
+ (name, grp, label, description, datatype)
+VALUES (
+ 'ui.staff.disable_links_newtabs',
+ 'gui',
+ oils_i18n_gettext('ui.staff.disable_links_newtabs',
+ 'Staff Client: no new tabs',
+ 'coust', 'label'),
+ oils_i18n_gettext('ui.staff.disable_links_newtabs',
+ 'Prevents links in the staff interface from opening in new tabs or windows.',
+ 'coust', 'description'),
+ 'bool'
+);
+
+
+SELECT evergreen.upgrade_deps_block_check('1467', :eg_version);
+
+CREATE TABLE action.eresource_link_click (
+ id BIGSERIAL PRIMARY KEY,
+ clicked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ url TEXT,
+ record BIGINT NOT NULL REFERENCES biblio.record_entry (id)
+);
+
+CREATE TABLE action.eresource_link_click_course (
+ id SERIAL PRIMARY KEY,
+ click BIGINT NOT NULL REFERENCES action.eresource_link_click (id) ON DELETE CASCADE,
+ course INT REFERENCES asset.course_module_course (id) ON UPDATE CASCADE ON DELETE SET NULL,
+ course_name TEXT NOT NULL,
+ course_number TEXT NOT NULL
+);
+
+INSERT INTO config.global_flag (name, label, enabled)
+ VALUES (
+ 'opac.eresources.link_click_tracking',
+ oils_i18n_gettext('opac.eresources.link_click_tracking',
+ 'Track clicks on eresources links. Before enabling this global flag, be sure that you are monitoring disk space on your database server and have a cron job set up to delete click records after the desired retention interval.',
+ 'cgf', 'label'),
+ FALSE
+ );
+
+CREATE FUNCTION action.delete_old_eresource_link_clicks(days integer)
+ RETURNS VOID AS
+ 'DELETE FROM action.eresource_link_click
+ WHERE clicked_at < current_timestamp
+ - ($1::text || '' days'')::interval'
+ LANGUAGE SQL
+ VOLATILE;
+
+
+
+SELECT evergreen.upgrade_deps_block_check('1468', :eg_version);
+
+-- Necessary pre-seed data
+
+
+INSERT INTO actor.passwd_type (code, name, login, crypt_algo, iter_count)
+ VALUES ('api', 'OpenAPI Integration Password', TRUE, 'bf', 10)
+ON CONFLICT DO NOTHING;
+
+-- Move top-level perms "down" ...
+INSERT INTO permission.grp_perm_map (grp,perm,depth,grantable)
+ SELECT DISTINCT g.id, p.perm, p.depth, p.grantable
+ FROM permission.grp_perm_map p,
+ permission.grp_tree g
+ WHERE g.parent = 1 AND p.grp = 1;
+
+-- ... then remove the User version ...
+DELETE FROM permission.grp_perm_map WHERE grp = 1;
+
+-- ... and add a new branch to the group tree for API perms
+INSERT INTO permission.grp_tree (name, parent, description, application_perm)
+ VALUES ('API Integrator', 1, 'API Integration Accounts', 'group_application.api_integrator');
+
+INSERT INTO permission.grp_tree (name, parent, description, application_perm) SELECT 'Patron API', id, 'Patron API', 'group_application.api_integrator' FROM permission.grp_tree WHERE name = 'API Integrator';
+INSERT INTO permission.grp_tree (name, parent, description, application_perm) SELECT 'Org Unit API', id, 'Org Unit API', 'group_application.api_integrator' FROM permission.grp_tree WHERE name = 'API Integrator';
+INSERT INTO permission.grp_tree (name, parent, description, application_perm) SELECT 'Bib Record API', id, 'Bib Record API', 'group_application.api_integrator' FROM permission.grp_tree WHERE name = 'API Integrator';
+INSERT INTO permission.grp_tree (name, parent, description, application_perm) SELECT 'Item Record API', id, 'Item Record API', 'group_application.api_integrator' FROM permission.grp_tree WHERE name = 'API Integrator';
+INSERT INTO permission.grp_tree (name, parent, description, application_perm) SELECT 'Holds API', id, 'Holds API', 'group_application.api_integrator' FROM permission.grp_tree WHERE name = 'API Integrator';
+INSERT INTO permission.grp_tree (name, parent, description, application_perm) SELECT 'Debt Collection API', id, 'Debt Collection API', 'group_application.api_integrator' FROM permission.grp_tree WHERE name = 'API Integrator';
+INSERT INTO permission.grp_tree (name, parent, description, application_perm) SELECT 'Course Reserves API', id, 'Course Reserves API', 'group_application.api_integrator' FROM permission.grp_tree WHERE name = 'API Integrator';
+
+INSERT INTO permission.perm_list (code) VALUES
+ ('group_application.api_integrator'),
+ ('API_LOGIN'),
+ ('REST.api'),
+ ('REST.api.patrons'),
+ ('REST.api.orgs'),
+ ('REST.api.bibs'),
+ ('REST.api.items'),
+ ('REST.api.holds'),
+ ('REST.api.collections'),
+ ('REST.api.courses')
+ --- ... etc
+ON CONFLICT DO NOTHING;
+
+INSERT INTO permission.grp_perm_map (grp,perm,depth)
+ SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='API Integrator' AND p.code IN ('API_LOGIN','REST.api')
+ UNION
+ SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Patron API' AND p.code = 'REST.api.patrons'
+ UNION
+ SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Org Unit API' AND p.code = 'REST.api.orgs'
+ UNION
+ SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Bib Record API' AND p.code = 'REST.api.bibs'
+ UNION
+ SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Item Record API' AND p.code = 'REST.api.items'
+ UNION
+ SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Holds API' AND p.code = 'REST.api.holds'
+ UNION
+ SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Debt Collection API' AND p.code = 'REST.api.collections'
+ UNION
+ SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Course Reserves API' AND p.code = 'REST.api.courses'
+ON CONFLICT DO NOTHING;
+
+DROP SCHEMA IF EXISTS openapi CASCADE;
+CREATE SCHEMA IF NOT EXISTS openapi;
+
+CREATE TABLE IF NOT EXISTS openapi.integrator (
+ id INT PRIMARY KEY REFERENCES actor.usr (id),
+ enabled BOOL NOT NULL DEFAULT TRUE
+);
+
+CREATE TABLE IF NOT EXISTS openapi.json_schema_datatype (
+ name TEXT PRIMARY KEY,
+ label TEXT NOT NULL UNIQUE,
+ description TEXT
+);
+INSERT INTO openapi.json_schema_datatype (name,label) VALUES
+ ('boolean','Boolean'),
+ ('string','String'),
+ ('integer','Integer'),
+ ('number','Number'),
+ ('array','Array'),
+ ('object','Object')
+ON CONFLICT DO NOTHING;
+
+CREATE TABLE IF NOT EXISTS openapi.json_schema_format (
+ name TEXT PRIMARY KEY,
+ label TEXT NOT NULL UNIQUE,
+ description TEXT
+);
+INSERT INTO openapi.json_schema_format (name,label) VALUES
+ ('date-time','Timestamp'),
+ ('date','Date'),
+ ('time','Time'),
+ ('interval','Interval'),
+ ('email','Email Address'),
+ ('uri','URI'),
+ ('identifier','Opaque Identifier'),
+ ('money','Money'),
+ ('float','Floating Point Number'),
+ ('int64','Large Integer'),
+ ('password','Password')
+ON CONFLICT DO NOTHING;
+
+CREATE TABLE IF NOT EXISTS openapi.rate_limit_definition (
+ id SERIAL PRIMARY KEY,
+ name TEXT UNIQUE NOT NULL, -- i18n
+ limit_interval INTERVAL NOT NULL,
+ limit_count INT NOT NULL
+);
+SELECT SETVAL('openapi.rate_limit_definition_id_seq'::TEXT, 100);
+INSERT INTO openapi.rate_limit_definition (id, name, limit_interval, limit_count) VALUES
+ (1, 'Once per second', '1 second', 1),
+ (2, 'Ten per minute', '1 minute', 10),
+ (3, 'One hunderd per hour', '1 hour', 100),
+ (4, 'One thousand per hour', '1 hour', 1000),
+ (5, 'One thousand per 24 hour period', '24 hours', 1000),
+ (6, 'Ten thousand per 24 hour period', '24 hours', 10000),
+ (7, 'Unlimited', '1 second', 1000000),
+ (8, 'One hundred per second', '1 second', 100)
+ON CONFLICT DO NOTHING;
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint (
+ operation_id TEXT PRIMARY KEY,
+ path TEXT NOT NULL,
+ http_method TEXT NOT NULL CHECK (http_method IN ('get','put','post','delete','patch')),
+ security TEXT NOT NULL DEFAULT 'bearerAuth' CHECK (security IN ('bearerAuth','basicAuth','cookieAuth','paramAuth')),
+ summary TEXT NOT NULL,
+ method_source TEXT NOT NULL, -- perl module or opensrf application, tested by regex and assumes opensrf app name contains a "."
+ method_name TEXT NOT NULL,
+ method_params TEXT, -- eg, 'eg_auth_token hold' or 'eg_auth_token eg_user_id circ req.json'
+ active BOOL NOT NULL DEFAULT TRUE,
+ rate_limit INT REFERENCES openapi.rate_limit_definition (id),
+ CONSTRAINT path_and_method_once UNIQUE (path, http_method)
+);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_param (
+ id SERIAL PRIMARY KEY,
+ endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ name TEXT NOT NULL CHECK (name ~ '^\w+$'),
+ required BOOL NOT NULL DEFAULT FALSE,
+ in_part TEXT NOT NULL DEFAULT 'query' CHECK (in_part IN ('path','query','header','cookie')),
+ fm_type TEXT,
+ schema_type TEXT REFERENCES openapi.json_schema_datatype (name),
+ schema_format TEXT REFERENCES openapi.json_schema_format (name),
+ array_items TEXT REFERENCES openapi.json_schema_datatype (name),
+ default_value TEXT,
+ CONSTRAINT endpoint_and_name_once UNIQUE (endpoint, name),
+ CONSTRAINT format_requires_type CHECK (schema_format IS NULL OR schema_type IS NOT NULL),
+ CONSTRAINT array_items_requires_array_type CHECK (array_items IS NULL OR schema_type = 'array')
+);
+CREATE TABLE IF NOT EXISTS openapi.endpoint_response (
+ id SERIAL PRIMARY KEY,
+ endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ validate BOOL NOT NULL DEFAULT TRUE,
+ status INT NOT NULL DEFAULT 200,
+ content_type TEXT NOT NULL DEFAULT 'application/json',
+ description TEXT NOT NULL DEFAULT 'Success',
+ fm_type TEXT,
+ schema_type TEXT REFERENCES openapi.json_schema_datatype (name),
+ schema_format TEXT REFERENCES openapi.json_schema_format (name),
+ array_items TEXT REFERENCES openapi.json_schema_datatype (name),
+ CONSTRAINT endpoint_status_content_type_once UNIQUE (endpoint, status, content_type)
+);
+
+CREATE TABLE IF NOT EXISTS openapi.perm_set (
+ id SERIAL PRIMARY KEY,
+ name TEXT NOT NULL
+); -- push sequence value
+SELECT SETVAL('openapi.perm_set_id_seq'::TEXT, 1001);
+INSERT INTO openapi.perm_set (id, name) VALUES
+ (1,'Self - API only'),
+ (2,'Patrons - API only'),
+ (3,'Orgs - API only'),
+ (4,'Bibs - API only'),
+ (5,'Items - API only'),
+ (6,'Holds - API only'),
+ (7,'Collections - API only'),
+ (8,'Courses - API only'),
+
+ (101,'Self - standard permissions'),
+ (102,'Patrons - standard permissions'),
+ (103,'Orgs - standard permissions'),
+ (104,'Bibs - standard permissions'),
+ (105,'Items - standard permissions'),
+ (106,'Holds - standard permissions'),
+ (107,'Collections - standard permissions'),
+ (108,'Courses - standard permissions')
+ON CONFLICT DO NOTHING;
+
+CREATE TABLE IF NOT EXISTS openapi.perm_set_perm_map (
+ id SERIAL PRIMARY KEY,
+ perm_set INT NOT NULL REFERENCES openapi.perm_set (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ perm INT NOT NULL REFERENCES permission.perm_list (id) ON UPDATE CASCADE ON DELETE CASCADE
+);
+INSERT INTO openapi.perm_set_perm_map (perm_set, perm)
+ SELECT 1, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api')
+ UNION
+ SELECT 2, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.patrons')
+ UNION
+ SELECT 3, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.orgs')
+ UNION
+ SELECT 4, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.bibs')
+ UNION
+ SELECT 5, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.items')
+ UNION
+ SELECT 6, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.holds')
+ UNION
+ SELECT 7, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.collections')
+ UNION
+ SELECT 8, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.cources')
+ UNION
+ -- ...
+ SELECT 101, id FROM permission.perm_list WHERE code IN ('OPAC_LOGIN')
+ UNION
+ SELECT 102, id FROM permission.perm_list WHERE code IN ('STAFF_LOGIN','VIEW_USER')
+ UNION
+ SELECT 103, id FROM permission.perm_list WHERE code IN ('OPAC_LOGIN')
+ UNION
+ SELECT 104, id FROM permission.perm_list WHERE code IN ('OPAC_LOGIN')
+ UNION
+ SELECT 105, id FROM permission.perm_list WHERE code IN ('OPAC_LOGIN')
+ UNION
+ SELECT 106, id FROM permission.perm_list WHERE code IN ('STAFF_LOGIN','VIEW_USER')
+ UNION
+ SELECT 107, id FROM permission.perm_list WHERE code IN ('STAFF_LOGIN','VIEW_USER')
+ UNION
+ SELECT 108, id FROM permission.perm_list WHERE code IN ('STAFF_LOGIN')
+
+ON CONFLICT DO NOTHING;
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_perm_set_map (
+ id SERIAL PRIMARY KEY,
+ endpoint TEXT NOT NULL REFERENCES openapi.endpoint(operation_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ perm_set INT NOT NULL REFERENCES openapi.perm_set (id) ON UPDATE CASCADE ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_set (
+ name TEXT PRIMARY KEY,
+ description TEXT NOT NULL,
+ active BOOL NOT NULL DEFAULT TRUE,
+ rate_limit INT REFERENCES openapi.rate_limit_definition (id)
+);
+INSERT INTO openapi.endpoint_set (name, description) VALUES
+ ('self', 'Methods for retrieving and manipulating your own user account information'),
+ ('orgs', 'Methods for retrieving and manipulating organizational unit information'),
+ ('patrons', 'Methods for retrieving and manipulating patron information'),
+ ('holds', 'Methods for accessing and manipulating hold data'),
+ ('collections', 'Methods for accessing and manipulating patron debt collections data'),
+ ('bibs', 'Methods for accessing and manipulating bibliographic records and related data'),
+ ('items', 'Methods for accessing and manipulating barcoded item records'),
+ ('courses', 'Methods for accessing and manipulating course reserve data')
+ON CONFLICT DO NOTHING;
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_user_rate_limit_map (
+ id SERIAL PRIMARY KEY,
+ accessor INT NOT NULL REFERENCES actor.usr (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ rate_limit INT NOT NULL REFERENCES openapi.rate_limit_definition (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT endpoint_accessor_once UNIQUE (accessor, endpoint)
+);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_set_user_rate_limit_map (
+ id SERIAL PRIMARY KEY,
+ accessor INT NOT NULL REFERENCES actor.usr (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ endpoint_set TEXT NOT NULL REFERENCES openapi.endpoint_set (name) ON UPDATE CASCADE ON DELETE CASCADE,
+ rate_limit INT NOT NULL REFERENCES openapi.rate_limit_definition (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT endpoint_set_accessor_once UNIQUE (accessor, endpoint_set)
+);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_ip_rate_limit_map (
+ id SERIAL PRIMARY KEY,
+ ip_range INET NOT NULL,
+ endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ rate_limit INT NOT NULL REFERENCES openapi.rate_limit_definition (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT endpoint_ip_range_once UNIQUE (ip_range, endpoint)
+);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_set_ip_rate_limit_map (
+ id SERIAL PRIMARY KEY,
+ ip_range INET NOT NULL,
+ endpoint_set TEXT NOT NULL REFERENCES openapi.endpoint_set (name) ON UPDATE CASCADE ON DELETE CASCADE,
+ rate_limit INT NOT NULL REFERENCES openapi.rate_limit_definition (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT endpoint_set_ip_range_once UNIQUE (ip_range, endpoint_set)
+);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_set_endpoint_map (
+ id SERIAL PRIMARY KEY,
+ endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ endpoint_set TEXT NOT NULL REFERENCES openapi.endpoint_set (name) ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT endpoint_set_endpoint_once UNIQUE (endpoint_set, endpoint)
+);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_perm_map (
+ id SERIAL PRIMARY KEY,
+ endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ perm INT NOT NULL REFERENCES permission.perm_list (id) ON UPDATE CASCADE ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_set_perm_set_map (
+ id SERIAL PRIMARY KEY,
+ endpoint_set TEXT NOT NULL REFERENCES openapi.endpoint_set (name) ON UPDATE CASCADE ON DELETE CASCADE,
+ perm_set INT NOT NULL REFERENCES openapi.perm_set (id) ON UPDATE CASCADE ON DELETE CASCADE
+);
+INSERT INTO openapi.endpoint_set_perm_set_map (endpoint_set, perm_set) VALUES
+ ('self', 1), ('self', 101),
+ ('patrons', 2), ('patrons', 102),
+ ('orgs', 3), ('orgs', 103),
+ ('bibs', 4), ('bibs', 104),
+ ('items', 5), ('items', 105),
+ ('holds', 6), ('holds', 106),
+ ('collections', 7), ('collections', 107),
+ ('courses', 8), ('courses', 108)
+ON CONFLICT DO NOTHING;
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_set_perm_map (
+ id SERIAL PRIMARY KEY,
+ endpoint_set TEXT NOT NULL REFERENCES openapi.endpoint_set (name) ON UPDATE CASCADE ON DELETE CASCADE,
+ perm INT NOT NULL REFERENCES permission.perm_list (id) ON UPDATE CASCADE ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS openapi.authen_attempt_log (
+ request_id TEXT PRIMARY KEY,
+ attempt_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ ip_addr INET,
+ cred_user TEXT,
+ token TEXT
+);
+CREATE INDEX authen_cred_user_attempt_time_idx ON openapi.authen_attempt_log (attempt_time, cred_user);
+CREATE INDEX authen_ip_addr_attempt_time_idx ON openapi.authen_attempt_log (attempt_time, ip_addr);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_access_attempt_log (
+ request_id TEXT PRIMARY KEY,
+ attempt_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ allowed BOOL NOT NULL,
+ ip_addr INET,
+ accessor INT REFERENCES actor.usr (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ token TEXT
+);
+CREATE INDEX access_accessor_attempt_time_idx ON openapi.endpoint_access_attempt_log (accessor, attempt_time);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_dispatch_log (
+ request_id TEXT PRIMARY KEY,
+ complete_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ error BOOL NOT NULL
+);
+
+CREATE OR REPLACE FUNCTION actor.verify_passwd(pw_usr integer, pw_type text, test_passwd text) RETURNS boolean AS $f$
+DECLARE
+ pw_salt TEXT;
+ api_enabled BOOL;
+BEGIN
+ /* Returns TRUE if the password provided matches the in-db password.
+ * If the password type is salted, we compare the output of CRYPT().
+ * NOTE: test_passwd is MD5(salt || MD5(password)) for legacy
+ * 'main' passwords.
+ *
+ * Password type 'api' requires that the user be enabled as an
+ * integrator in the openapi.integrator table.
+ */
+
+ IF pw_type = 'api' THEN
+ SELECT enabled INTO api_enabled
+ FROM openapi.integrator
+ WHERE id = pw_usr;
+
+ IF NOT FOUND OR api_enabled IS FALSE THEN
+ -- API integrator account not registered
+ RETURN FALSE;
+ END IF;
+ END IF;
+
+ SELECT INTO pw_salt salt FROM actor.passwd
+ WHERE usr = pw_usr AND passwd_type = pw_type;
+
+ IF NOT FOUND THEN
+ -- no such password
+ RETURN FALSE;
+ END IF;
+
+ IF pw_salt IS NULL THEN
+ -- Password is unsalted, compare the un-CRYPT'ed values.
+ RETURN EXISTS (
+ SELECT TRUE FROM actor.passwd WHERE
+ usr = pw_usr AND
+ passwd_type = pw_type AND
+ passwd = test_passwd
+ );
+ END IF;
+
+ RETURN EXISTS (
+ SELECT TRUE FROM actor.passwd WHERE
+ usr = pw_usr AND
+ passwd_type = pw_type AND
+ passwd = CRYPT(test_passwd, pw_salt)
+ );
+END;
+$f$ STRICT LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION openapi.find_default_endpoint_rate_limit (target_endpoint TEXT) RETURNS openapi.rate_limit_definition AS $f$
+DECLARE
+ def_rl openapi.rate_limit_definition%ROWTYPE;
+BEGIN
+ -- Default rate limits can be applied at the endpoint or endpoint_set level;
+ -- endpoint overrides endpoint_set, and we choose the most restrictive from
+ -- the set if we have to look there.
+ SELECT d.* INTO def_rl
+ FROM openapi.rate_limit_definition d
+ JOIN openapi.endpoint e ON (e.rate_limit = d.id)
+ WHERE e.operation_id = target_endpoint;
+
+ IF NOT FOUND THEN
+ SELECT d.* INTO def_rl
+ FROM openapi.rate_limit_definition d
+ JOIN openapi.endpoint_set es ON (es.rate_limit = d.id)
+ JOIN openapi.endpoint_set_endpoint_map m ON (es.name = m.endpoint_set AND m.endpoint = target_endpoint)
+ -- This ORDER BY calculates the avg time between requests the user would have to wait to perfectly
+ -- avoid rate limiting. So, a bigger wait means it's more restrictive. We take the most restrictive
+ -- set-applied one.
+ ORDER BY EXTRACT(EPOCH FROM d.limit_interval) / d.limit_count::NUMERIC DESC
+ LIMIT 1;
+ END IF;
+
+ -- If there's no default for the endpoint or set, we provide 1/sec.
+ IF NOT FOUND THEN
+ def_rl.limit_interval := '1 second'::INTERVAL;
+ def_rl.limit_count := 1;
+ END IF;
+
+ RETURN def_rl;
+END;
+$f$ STABLE LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION openapi.find_user_endpoint_rate_limit (target_endpoint TEXT, accessing_usr INT) RETURNS openapi.rate_limit_definition AS $f$
+DECLARE
+ def_u_rl openapi.rate_limit_definition%ROWTYPE;
+BEGIN
+ SELECT d.* INTO def_u_rl
+ FROM openapi.rate_limit_definition d
+ JOIN openapi.endpoint_user_rate_limit_map e ON (e.rate_limit = d.id)
+ WHERE e.endpoint = target_endpoint
+ AND e.accessor = accessing_usr;
+
+ IF NOT FOUND THEN
+ SELECT d.* INTO def_u_rl
+ FROM openapi.rate_limit_definition d
+ JOIN openapi.endpoint_set_user_rate_limit_map e ON (e.rate_limit = d.id AND e.accessor = accessing_usr)
+ JOIN openapi.endpoint_set_endpoint_map m ON (e.endpoint_set = m.endpoint_set AND m.endpoint = target_endpoint)
+ ORDER BY EXTRACT(EPOCH FROM d.limit_interval) / d.limit_count::NUMERIC DESC
+ LIMIT 1;
+ END IF;
+
+ RETURN def_u_rl;
+END;
+$f$ STABLE LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION openapi.find_ip_addr_endpoint_rate_limit (target_endpoint TEXT, from_ip_addr INET) RETURNS openapi.rate_limit_definition AS $f$
+DECLARE
+ def_i_rl openapi.rate_limit_definition%ROWTYPE;
+BEGIN
+ SELECT d.* INTO def_i_rl
+ FROM openapi.rate_limit_definition d
+ JOIN openapi.endpoint_ip_rate_limit_map e ON (e.rate_limit = d.id)
+ WHERE e.endpoint = target_endpoint
+ AND e.ip_range && from_ip_addr
+ -- For IPs, we order first by the size of the ranges that we
+ -- matched (mask length), most specific (smallest block of IPs)
+ -- first, then by the restrictiveness of the limit, more restrictive first.
+ ORDER BY MASKLEN(e.ip_range) DESC, EXTRACT(EPOCH FROM d.limit_interval) / d.limit_count::NUMERIC DESC
+ LIMIT 1;
+
+ IF NOT FOUND THEN
+ SELECT d.* INTO def_i_rl
+ FROM openapi.rate_limit_definition d
+ JOIN openapi.endpoint_set_ip_rate_limit_map e ON (e.rate_limit = d.id AND ip_range && from_ip_addr)
+ JOIN openapi.endpoint_set_endpoint_map m ON (e.endpoint_set = m.endpoint_set AND m.endpoint = target_endpoint)
+ ORDER BY MASKLEN(e.ip_range) DESC, EXTRACT(EPOCH FROM d.limit_interval) / d.limit_count::NUMERIC DESC
+ LIMIT 1;
+ END IF;
+
+ RETURN def_i_rl;
+END;
+$f$ STABLE LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION openapi.check_generic_endpoint_rate_limit (target_endpoint TEXT, accessing_usr INT DEFAULT NULL, from_ip_addr INET DEFAULT NULL) RETURNS INT AS $f$
+DECLARE
+ def_rl openapi.rate_limit_definition%ROWTYPE;
+ def_u_rl openapi.rate_limit_definition%ROWTYPE;
+ def_i_rl openapi.rate_limit_definition%ROWTYPE;
+ u_wait INT;
+ i_wait INT;
+BEGIN
+ def_rl := openapi.find_default_endpoint_rate_limit(target_endpoint);
+
+ IF accessing_usr IS NOT NULL THEN
+ def_u_rl := openapi.find_user_endpoint_rate_limit(target_endpoint, accessing_usr);
+ END IF;
+
+ IF from_ip_addr IS NOT NULL THEN
+ def_i_rl := openapi.find_ip_addr_endpoint_rate_limit(target_endpoint, from_ip_addr);
+ END IF;
+
+ -- Now we test the user-based and IP-based limits in their focused way...
+ IF def_u_rl.id IS NOT NULL THEN
+ SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_u_rl.limit_interval) - NOW())) INTO u_wait
+ FROM (SELECT l.attempt_time,
+ COUNT(*) OVER (PARTITION BY l.accessor ORDER BY l.attempt_time DESC) AS running_count
+ FROM openapi.endpoint_access_attempt_log l
+ WHERE l.endpoint = target_endpoint
+ AND l.accessor = accessing_usr
+ AND l.attempt_time > NOW() - def_u_rl.limit_interval
+ ) x
+ WHERE running_count = def_u_rl.limit_count;
+ END IF;
+
+ IF def_i_rl.id IS NOT NULL THEN
+ SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_u_rl.limit_interval) - NOW())) INTO i_wait
+ FROM (SELECT l.attempt_time,
+ COUNT(*) OVER (PARTITION BY l.ip_addr ORDER BY l.attempt_time DESC) AS running_count
+ FROM openapi.endpoint_access_attempt_log l
+ WHERE l.endpoint = target_endpoint
+ AND l.ip_addr = from_ip_addr
+ AND l.attempt_time > NOW() - def_u_rl.limit_interval
+ ) x
+ WHERE running_count = def_u_rl.limit_count;
+ END IF;
+
+ -- If there are no user-specific or IP-based limit
+ -- overrides; check endpoint-wide limits for user,
+ -- then IP, and if we were passed neither, then limit
+ -- endpoint access for all users. Better to lock it
+ -- all down than to set the servers on fire.
+ IF COALESCE(u_wait, i_wait) IS NULL AND COALESCE(def_i_rl.id, def_u_rl.id) IS NULL THEN
+ IF accessing_usr IS NOT NULL THEN
+ SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_rl.limit_interval) - NOW())) INTO u_wait
+ FROM (SELECT l.attempt_time,
+ COUNT(*) OVER (PARTITION BY l.accessor ORDER BY l.attempt_time DESC) AS running_count
+ FROM openapi.endpoint_access_attempt_log l
+ WHERE l.endpoint = target_endpoint
+ AND l.accessor = accessing_usr
+ AND l.attempt_time > NOW() - def_rl.limit_interval
+ ) x
+ WHERE running_count = def_rl.limit_count;
+ ELSIF from_ip_addr IS NOT NULL THEN
+ SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_rl.limit_interval) - NOW())) INTO i_wait
+ FROM (SELECT l.attempt_time,
+ COUNT(*) OVER (PARTITION BY l.ip_addr ORDER BY l.attempt_time DESC) AS running_count
+ FROM openapi.endpoint_access_attempt_log l
+ WHERE l.endpoint = target_endpoint
+ AND l.ip_addr = from_ip_addr
+ AND l.attempt_time > NOW() - def_rl.limit_interval
+ ) x
+ WHERE running_count = def_rl.limit_count;
+ ELSE -- we have no user and no IP, global per-endpoint rate limit?
+ SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_rl.limit_interval) - NOW())) INTO i_wait
+ FROM (SELECT l.attempt_time,
+ COUNT(*) OVER (PARTITION BY l.endpoint ORDER BY l.attempt_time DESC) AS running_count
+ FROM openapi.endpoint_access_attempt_log l
+ WHERE l.endpoint = target_endpoint
+ AND l.attempt_time > NOW() - def_rl.limit_interval
+ ) x
+ WHERE running_count = def_rl.limit_count;
+ END IF;
+ END IF;
+
+ -- Send back the largest required wait time, or NULL for no restriction
+ u_wait := GREATEST(u_wait,i_wait);
+ IF u_wait > 0 THEN
+ RETURN u_wait;
+ END IF;
+
+ RETURN NULL;
+END;
+$f$ STABLE LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION openapi.check_auth_endpoint_rate_limit (accessing_usr TEXT DEFAULT NULL, from_ip_addr INET DEFAULT NULL) RETURNS INT AS $f$
+DECLARE
+ def_rl openapi.rate_limit_definition%ROWTYPE;
+ def_u_rl openapi.rate_limit_definition%ROWTYPE;
+ def_i_rl openapi.rate_limit_definition%ROWTYPE;
+ u_wait INT;
+ i_wait INT;
+BEGIN
+ def_rl := openapi.find_default_endpoint_rate_limit('authenticateUser');
+
+ IF accessing_usr IS NOT NULL THEN
+ SELECT (openapi.find_user_endpoint_rate_limit('authenticateUser', u.id)).* INTO def_u_rl
+ FROM actor.usr u
+ WHERE u.usrname = accessing_usr;
+ END IF;
+
+ IF from_ip_addr IS NOT NULL THEN
+ def_i_rl := openapi.find_ip_addr_endpoint_rate_limit('authenticateUser', from_ip_addr);
+ END IF;
+
+ -- Now we test the user-based and IP-based limits in their focused way...
+ IF def_u_rl.id IS NOT NULL THEN
+ SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_u_rl.limit_interval) - NOW())) INTO u_wait
+ FROM (SELECT l.attempt_time,
+ COUNT(*) OVER (PARTITION BY l.cred_user ORDER BY l.attempt_time DESC) AS running_count
+ FROM openapi.authen_attempt_log l
+ WHERE l.cred_user = accessing_usr
+ AND l.attempt_time > NOW() - def_u_rl.limit_interval
+ ) x
+ WHERE running_count = def_u_rl.limit_count;
+ END IF;
+
+ IF def_i_rl.id IS NOT NULL THEN
+ SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_u_rl.limit_interval) - NOW())) INTO i_wait
+ FROM (SELECT l.attempt_time,
+ COUNT(*) OVER (PARTITION BY l.ip_addr ORDER BY l.attempt_time DESC) AS running_count
+ FROM openapi.authen_attempt_log l
+ WHERE l.ip_addr = from_ip_addr
+ AND l.attempt_time > NOW() - def_u_rl.limit_interval
+ ) x
+ WHERE running_count = def_u_rl.limit_count;
+ END IF;
+
+ -- If there are no user-specific or IP-based limit
+ -- overrides; check endpoint-wide limits for user,
+ -- then IP, and if we were passed neither, then limit
+ -- endpoint access for all users. Better to lock it
+ -- all down than to set the servers on fire.
+ IF COALESCE(u_wait, i_wait) IS NULL AND COALESCE(def_i_rl.id, def_u_rl.id) IS NULL THEN
+ IF accessing_usr IS NOT NULL THEN
+ SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_rl.limit_interval) - NOW())) INTO u_wait
+ FROM (SELECT l.attempt_time,
+ COUNT(*) OVER (PARTITION BY l.cred_user ORDER BY l.attempt_time DESC) AS running_count
+ FROM openapi.authen_attempt_log l
+ WHERE l.cred_user = accessing_usr
+ AND l.attempt_time > NOW() - def_rl.limit_interval
+ ) x
+ WHERE running_count = def_rl.limit_count;
+ ELSIF from_ip_addr IS NOT NULL THEN
+ SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_rl.limit_interval) - NOW())) INTO i_wait
+ FROM (SELECT l.attempt_time,
+ COUNT(*) OVER (PARTITION BY l.ip_addr ORDER BY l.attempt_time DESC) AS running_count
+ FROM openapi.authen_attempt_log l
+ WHERE l.ip_addr = from_ip_addr
+ AND l.attempt_time > NOW() - def_rl.limit_interval
+ ) x
+ WHERE running_count = def_rl.limit_count;
+ ELSE -- we have no user and no IP, global auth attempt rate limit?
+ SELECT CEIL(EXTRACT(EPOCH FROM (l.attempt_time + def_rl.limit_interval) - NOW())) INTO u_wait
+ FROM openapi.authen_attempt_log l
+ WHERE l.attempt_time > NOW() - def_rl.limit_interval
+ ORDER BY l.attempt_time DESC
+ LIMIT 1 OFFSET def_rl.limit_count;
+ END IF;
+ END IF;
+
+ -- Send back the largest required wait time, or NULL for no restriction
+ u_wait := GREATEST(u_wait,i_wait);
+ IF u_wait > 0 THEN
+ RETURN u_wait;
+ END IF;
+
+ RETURN NULL;
+END;
+$f$ STABLE LANGUAGE PLPGSQL;
+
+--===================================== Seed data ==================================
+
+-- ===== authentication
+INSERT INTO openapi.endpoint (operation_id, path, security, http_method, summary, method_source, method_name, method_params, rate_limit) VALUES
+-- Builtin auth-related methods, give all users and IPs 100/s of each
+ ('authenticateUser', '/self/auth', 'basicAuth', 'get', 'Authenticate API user', 'OpenILS::OpenAPI::Controller', 'authenticateUser', 'param.u param.p param.t', 8),
+ ('logoutUser', '/self/auth', 'bearerAuth', 'delete', 'Logout API user', 'open-ils.auth', 'open-ils.auth.session.delete', 'eg_auth_token', 8)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value) VALUES
+ ('authenticateUser','u','query','string',NULL,NULL),
+ ('authenticateUser','p','query','string','password',NULL),
+ ('authenticateUser','t','query','string',NULL,'api')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('authenticateUser','object') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,content_type) VALUES ('authenticateUser','text/plain') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('logoutUser','object') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,content_type) VALUES ('logoutUser','text/plain') ON CONFLICT DO NOTHING;
+
+-- ===== self-service
+-- get/update me
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrieveSelfProfile', '/self/me', 'get', 'Return patron/user record for logged in user', 'OpenILS::OpenAPI::Controller::patron', 'deliver_user', 'eg_auth_token eg_user_id'),
+ ('selfUpdateParts', '/self/me', 'patch', 'Update portions of the logged in user''s record', 'OpenILS::OpenAPI::Controller::patron', 'update_user_parts', 'eg_auth_token req.json')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveSelfProfile','au') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('selfUpdateParts','object') ON CONFLICT DO NOTHING;
+
+-- get my standing penalties
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'selfActivePenalties',
+ '/self/standing_penalties',
+ 'get',
+ 'Produces patron-visible penalty details for the authorized account',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'standing_penalties',
+ 'eg_auth_token eg_user_id "1"'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('selfActivePenalties','array','object') ON CONFLICT DO NOTHING; -- array of fleshed ausp
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'selfPenalty',
+ '/self/standing_penalty/:penaltyid',
+ 'get',
+ 'Retrieve one penalty for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'standing_penalties',
+ 'eg_auth_token eg_user_id "1" param.penaltyid'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('selfPenalty','penaltyid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+
+
+
+-- manage my holds
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrieveSelfHolds', '/self/holds', 'get', 'Return unfilled holds for the authorized account', 'OpenILS::OpenAPI::Controller::hold', 'open_holds', 'eg_auth_token eg_user_id'),
+ ('requestSelfHold', '/self/holds', 'post', 'Request a hold for the authorized account', 'OpenILS::OpenAPI::Controller::hold', 'request_hold', 'eg_auth_token eg_user_id req.json'),
+ ('retrieveSelfHold', '/self/hold/:hold', 'get', 'Retrieve one hold for the logged in user', 'OpenILS::OpenAPI::Controller::hold', 'fetch_user_hold', 'eg_auth_token eg_user_id param.hold'),
+ ('updateSelfHold', '/self/hold/:hold', 'patch', 'Update one hold for the logged in user', 'OpenILS::OpenAPI::Controller::hold', 'update_user_hold', 'eg_auth_token eg_user_id param.hold req.json'),
+ ('cancelSelfHold', '/self/hold/:hold', 'delete', 'Cancel one hold for the logged in user', 'OpenILS::OpenAPI::Controller::hold', 'cancel_user_hold', 'eg_auth_token eg_user_id param.hold "6"')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES
+ ('retrieveSelfHold','hold','path','integer',TRUE),
+ ('updateSelfHold','hold','path','integer',TRUE),
+ ('cancelSelfHold','hold','path','integer',TRUE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveSelfHolds','array','object') ON CONFLICT DO NOTHING;
+
+
+-- general xact list
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrieveSelfXacts',
+ '/self/transactions/:state',
+ 'get',
+ 'Produces a list of transactions of the logged in user',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'transactions_by_state',
+ 'eg_auth_token eg_user_id state param.limit param.offset param.sort param.before param.after'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value,required) VALUES
+ ('retrieveSelfXacts','state','path','string',NULL,NULL,TRUE),
+ ('retrieveSelfXacts','limit','query','integer',NULL,NULL,FALSE),
+ ('retrieveSelfXacts','offset','query','integer',NULL,'0',FALSE),
+ ('retrieveSelfXacts','sort','query','string',NULL,'desc',FALSE),
+ ('retrieveSelfXacts','before','query','string','date-time',NULL,FALSE),
+ ('retrieveSelfXacts','after','query','string','date-time',NULL,FALSE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveSelfXacts','array','object') ON CONFLICT DO NOTHING;
+
+-- general xact detail
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrieveSelfXact',
+ '/self/transaction/:id',
+ 'get',
+ 'Details of one transaction for the logged in user',
+ 'open-ils.actor',
+ 'open-ils.actor.user.transaction.fleshed.retrieve',
+ 'eg_auth_token param.id'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveSelfXact','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrieveSelfCircs', '/self/checkouts', 'get', 'Open Circs for the logged in user', 'open-ils.circ', 'open-ils.circ.actor.user.checked_out.atomic', 'eg_auth_token eg_user_id'),
+ ('requestSelfCirc', '/self/checkouts', 'post', 'Attempt a circulation for the logged in user', 'OpenILS::OpenAPI::Controller::patron', 'checkout_item', 'eg_auth_token eg_user_id req.json')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveSelfCircs','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrieveSelfCircHistory',
+ '/self/checkouts/history',
+ 'get',
+ 'Historical Circs for logged in user',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'circulation_history',
+ 'eg_auth_token eg_user_id param.limit param.offset param.sort param.before param.after'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value) VALUES
+ ('retrieveSelfCircHistory','limit','query','integer',NULL,NULL),
+ ('retrieveSelfCircHistory','offset','query','integer',NULL,'0'),
+ ('retrieveSelfCircHistory','sort','query','string',NULL,'desc'),
+ ('retrieveSelfCircHistory','before','query','string','date-time',NULL),
+ ('retrieveSelfCircHistory','after','query','string','date-time',NULL)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveSelfCircHistory','array','object') ON CONFLICT DO NOTHING;
+
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrieveSelfCirc', '/self/checkout/:id', 'get', 'Retrieve one circulation for the logged in user', 'open-ils.actor', 'open-ils.actor.user.transaction.fleshed.retrieve', 'eg_auth_token param.id'),
+ ('renewSelfCirc', '/self/checkout/:id', 'put', 'Renew one circulation for the logged in user', 'OpenILS::OpenAPI::Controller::patron', 'renew_circ', 'eg_auth_token param.id eg_user_id'),
+ ('checkinSelfCirc', '/self/checkout/:id', 'delete', 'Check in one circulation for the logged in user', 'OpenILS::OpenAPI::Controller::patron', 'checkin_circ', 'eg_auth_token param.id eg_user_id')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES
+ ('retrieveSelfCirc','id','path','integer',TRUE),
+ ('renewSelfCirc','id','path','integer',TRUE),
+ ('checkinSelfCirc','id','path','integer',TRUE)
+ON CONFLICT DO NOTHING;
+
+-- bib, item, and org methods
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrieveOrgList',
+ '/org_units',
+ 'get',
+ 'List of org units',
+ 'OpenILS::OpenAPI::Controller::org',
+ 'flat_org_list',
+ 'every_param.field every_param.comparison every_param.value'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES
+ ('retrieveOrgList','field','query','string'),
+ ('retrieveOrgList','comparison','query','string'),
+ ('retrieveOrgList','value','query','string')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveOrgList','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrieveOneOrg',
+ '/org_unit/:id',
+ 'get',
+ 'One org unit',
+ 'OpenILS::OpenAPI::Controller::org',
+ 'one_org',
+ 'param.id'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveOneOrg','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveOneOrg','aou') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name) VALUES (
+ 'retrieveFullOrgTree',
+ '/org_tree',
+ 'get',
+ 'Full hierarchical tree of org units',
+ 'OpenILS::OpenAPI::Controller::org',
+ 'full_tree'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveFullOrgTree','aou') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrievePartialOrgTree',
+ '/org_tree/:id',
+ 'get',
+ 'Partial hierarchical tree of org units starting from a specific org unit',
+ 'OpenILS::OpenAPI::Controller::org',
+ 'one_tree',
+ 'param.id'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrievePartialOrgTree','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrievePartialOrgTree','aou') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'createOneBib',
+ '/bibs',
+ 'post',
+ 'Attempts to create a bibliographic record using MARCXML passed as the request content',
+ 'open-ils.cat',
+ 'open-ils.cat.biblio.record.xml.create',
+ 'eg_auth_token req.text param.sourcename'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES ('createOneBib','sourcename','query','string') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('createOneBib','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'updateOneBib',
+ '/bib/:id',
+ 'put',
+ 'Attempts to update a bibliographic record using MARCXML passed as the request content',
+ 'open-ils.cat',
+ 'open-ils.cat.biblio.record.marc.replace',
+ 'eg_auth_token param.id req.text param.sourcename'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('updateOneBib','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES ('updateOneBib','sourcename','query','string') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('updateOneBib','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'deleteOneBib',
+ '/bib/:id',
+ 'delete',
+ 'Attempts to delete a bibliographic record',
+ 'open-ils.cat',
+ 'open-ils.cat.biblio.record_entry.delete',
+ 'eg_auth_token param.id'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('deleteOneBib','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,validate) VALUES ('deleteOneBib','integer',FALSE) ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'updateBREParts',
+ '/bib/:id',
+ 'patch',
+ 'Attempts to update the biblio.record_entry metadata surrounding a bib record',
+ 'OpenILS::OpenAPI::Controller::bib',
+ 'update_bre_parts',
+ 'eg_auth_token param.id req.json'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('updateBREParts','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('updateBREParts','bre') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrieveOneBib',
+ '/bib/:id',
+ 'get',
+ 'Retrieve a bibliographic record, either full biblio::record_entry object, or just the MARCXML',
+ 'OpenILS::OpenAPI::Controller::bib',
+ 'fetch_one_bib',
+ 'param.id'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveOneBib','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveOneBib','bre') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,content_type) VALUES ('retrieveOneBib','application/xml') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,content_type) VALUES ('retrieveOneBib','application/octet-stream') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrieveOneBibHoldings',
+ '/bib/:id/holdings',
+ 'get',
+ 'Retrieve the holdings data for a bibliographic record',
+ 'OpenILS::OpenAPI::Controller::bib',
+ 'fetch_one_bib_holdings',
+ 'param.id'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,default_value,required) VALUES
+ ('retrieveOneBibHoldings','id','path','integer',NULL,TRUE),
+ ('retrieveOneBibHoldings','limit','query','integer',NULL,FALSE),
+ ('retrieveOneBibHoldings','offset','query','integer','0',FALSE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveOneBibHoldings','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'bibDisplayFields',
+ '/bib/:id/display_fields',
+ 'get',
+ 'Retrieve display-related data for a bibliographic record',
+ 'OpenILS::OpenAPI::Controller::bib',
+ 'fetch_one_bib_display_fields',
+ 'param.id req.text'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('bibDisplayFields','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('bibDisplayFields','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'newItems',
+ '/items/fresh',
+ 'get',
+ 'Retrieve a list of newly added items',
+ 'OpenILS::OpenAPI::Controller::bib',
+ 'fetch_new_items',
+ 'param.limit param.offset param.maxage'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,default_value) VALUES ('newItems','limit','query','integer','0') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,default_value) VALUES ('newItems','offset','query','integer','100') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format) VALUES ('newItems','maxage','query','string','interval') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('newItems','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrieveItem',
+ '/item/:barcode',
+ 'get',
+ 'Retrieve one item by its barcode',
+ 'OpenILS::OpenAPI::Controller::bib',
+ 'item_by_barcode',
+ 'param.barcode'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveItem','barcode','path','string',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,description,status,schema_type) VALUES ('retrieveItem','Item Lookup Failed','404','object') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveItem','acp') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'createItem',
+ '/items',
+ 'post',
+ 'Create an item record',
+ 'OpenILS::OpenAPI::Controller::bib',
+ 'create_or_update_one_item',
+ 'eg_auth_token req.json'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,description,status) VALUES ('createItem','Item Creation Failed','400') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('createItem','acp') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'updateItem',
+ '/item/:barcode',
+ 'patch',
+ 'Update a restricted set of item record fields',
+ 'OpenILS::OpenAPI::Controller::bib',
+ 'create_or_update_one_item',
+ 'eg_auth_token req.json param.barcode'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('updateItem','barcode','path','string',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,description,status) VALUES ('updateItem','Item Update Failed','400') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('updateItem','acp') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'deleteItem',
+ '/item/:barcode',
+ 'delete',
+ 'Delete one item record',
+ 'OpenILS::OpenAPI::Controller::bib',
+ 'delete_one_item',
+ 'eg_auth_token param.barcode'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('deleteItem','barcode','path','string',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,description,status,schema_type) VALUES ('deleteItem','Item Deletion Failed','404','object') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('deleteItem','boolean') ON CONFLICT DO NOTHING;
+
+
+-- === patron (non-self) methods
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'searchPatrons',
+ '/patrons',
+ 'get',
+ 'List of patrons matching requested conditions',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'find_users',
+ 'eg_auth_token every_param.field every_param.comparison every_param.value param.limit param.offset'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES
+ ('searchPatrons','field','query','string'),
+ ('searchPatrons','comparison','query','string'),
+ ('searchPatrons','value','query','string')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value) VALUES
+ ('searchPatrons','offset','query','integer',NULL,'0'),
+ ('searchPatrons','limit','query','integer',NULL,'100')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('searchPatrons','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'verifyUserCredentials',
+ '/patrons/verify',
+ 'get',
+ 'Verify the credentials for a user account',
+ 'open-ils.actor',
+ 'open-ils.actor.verify_user_password',
+ 'eg_auth_token param.barcode param.usrname "" param.password'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,required) VALUES
+ ('verifyUserCredentials','barcode','query','string',NULL,FALSE),
+ ('verifyUserCredentials','usrname','query','string',NULL,FALSE),
+ ('verifyUserCredentials','password','query','string','password',TRUE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('verifyUserCredentials','boolean') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrievePatronProfile', '/patron/:userid', 'get', 'Return patron/user record for the requested user', 'OpenILS::OpenAPI::Controller::patron', 'deliver_user', 'eg_auth_token param.userid')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrievePatronProfile','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrievePatronProfile','au') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronIdByCardBarcode',
+ '/patrons/by_barcode/:barcode/id',
+ 'get',
+ 'Retrieve patron id by barcode',
+ 'open-ils.actor',
+ 'open-ils.actor.user.retrieve_id_by_barcode_or_username',
+ 'eg_auth_token param.barcode'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronIdByCardBarcode','barcode','path','string',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('patronIdByCardBarcode','integer') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronIdByUsername',
+ '/patrons/by_username/:username/id',
+ 'get',
+ 'Retrieve patron id by username',
+ 'open-ils.actor',
+ 'open-ils.actor.user.retrieve_id_by_barcode_or_username',
+ 'eg_auth_token "" param.username'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronIdByUsername','username','path','string',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('patronIdByUsername','integer') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronByCardBarcode',
+ '/patrons/by_barcode/:barcode',
+ 'get',
+ 'Retrieve patron by barcode',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'user_by_identifier_string',
+ 'eg_auth_token param.barcode'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronByCardBarcode','barcode','path','string',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('patronByCardBarcode','au') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronByUsername',
+ '/patrons/by_username/:username',
+ 'get',
+ 'Retrieve patron by username',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'user_by_identifier_string',
+ 'eg_auth_token "" param.username'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronByUsername','username','path','string',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('patronByUsername','au') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrievePatronCircHistory',
+ '/patron/:userid/checkouts/history',
+ 'get',
+ 'Historical Circs for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'circulation_history',
+ 'eg_auth_token param.userid param.limit param.offset param.sort param.before param.after'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value,required) VALUES
+ ('retrievePatronCircHistory','userid','path','integer',NULL,NULL,TRUE),
+ ('retrievePatronCircHistory','limit','query','integer',NULL,NULL,FALSE),
+ ('retrievePatronCircHistory','offset','query','integer',NULL,'0',FALSE),
+ ('retrievePatronCircHistory','sort','query','string',NULL,'desc',FALSE),
+ ('retrievePatronCircHistory','before','query','string','date-time',NULL,FALSE),
+ ('retrievePatronCircHistory','after','query','string','date-time',NULL,FALSE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrievePatronCircHistory','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrievePatronHolds', '/patron/:userid/holds', 'get', 'Retrieve unfilled holds for a patron', 'OpenILS::OpenAPI::Controller::hold', 'open_holds', 'eg_auth_token param.userid'),
+ ('requestPatronHold', '/patron/:userid/holds', 'post', 'Request a hold for a patron', 'OpenILS::OpenAPI::Controller::hold', 'request_hold', 'eg_auth_token param.userid req.json'),
+ ('retrievePatronHold','/patron/:userid/hold/:hold', 'get', 'Retrieve one hold for a patron', 'OpenILS::OpenAPI::Controller::hold', 'fetch_user_hold', 'eg_auth_token param.userid param.hold'),
+ ('updatePatronHold', '/patron/:userid/hold/:hold', 'patch', 'Update one hold for a patron', 'OpenILS::OpenAPI::Controller::hold', 'update_user_hold', 'eg_auth_token param.userid param.hold req.json'),
+ ('cancelPatronHold', '/patron/:userid/hold/:hold', 'delete', 'Cancel one hold for a patron', 'OpenILS::OpenAPI::Controller::hold', 'cancel_user_hold', 'eg_auth_token param.userid param.hold "6"')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES
+ ('retrievePatronHolds','userid','path','integer',TRUE),
+ ('retrievePatronHold','userid','path','integer',TRUE),
+ ('retrievePatronHold','hold','path','integer',TRUE),
+ ('requestPatronHold','userid','path','integer',TRUE),
+ ('updatePatronHold','userid','path','integer',TRUE),
+ ('updatePatronHold','hold','path','integer',TRUE),
+ ('cancelPatronHold','userid','path','integer',TRUE),
+ ('cancelPatronHold','hold','path','integer',TRUE)
+ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrieveHoldPickupLocations', '/holds/pickup_locations', 'get', 'Retrieve all valid hold/reserve pickup locations', 'OpenILS::OpenAPI::Controller::hold', 'valid_hold_pickup_locations', '')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveHoldPickupLocations','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrieveHold', '/hold/:hold', 'get', 'Retrieve one hold object', 'open-ils.circ', 'open-ils.circ.hold.details.retrieve', 'eg_auth_token param.hold')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveHold','hold','path','integer',TRUE) ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrievePatronXacts',
+ '/patron/:userid/transactions/:state',
+ 'get',
+ 'Produces a list of transactions of the specified user',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'transactions_by_state',
+ 'eg_auth_token param.userid state param.limit param.offset param.sort param.before param.after'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value,required) VALUES
+ ('retrievePatronXacts','userid','path','integer',NULL,NULL,TRUE),
+ ('retrievePatronXacts','state','path','string',NULL,NULL,TRUE),
+ ('retrievePatronXacts','limit','query','integer',NULL,NULL,FALSE),
+ ('retrievePatronXacts','offset','query','integer',NULL,'0',FALSE),
+ ('retrievePatronXacts','sort','query','string',NULL,'desc',FALSE),
+ ('retrievePatronXacts','before','query','string','date-time',NULL,FALSE),
+ ('retrievePatronXacts','after','query','string','date-time',NULL,FALSE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrievePatronXacts','array','object') ON CONFLICT DO NOTHING;
+
+-- general xact detail
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrievePatronXact',
+ '/patron/:userid/transaction/:id',
+ 'get',
+ 'Details of one transaction for the specified user',
+ 'open-ils.actor',
+ 'open-ils.actor.user.transaction.fleshed.retrieve',
+ 'eg_auth_token param.id'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrievePatronXact','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrievePatronXact','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrievePatronCircs', '/patron/:userid/checkouts', 'get', 'Open Circs for a patron', 'open-ils.circ', 'open-ils.circ.actor.user.checked_out.atomic', 'eg_auth_token param.userid'),
+ ('requestPatronCirc', '/patron/:userid/checkouts', 'post', 'Attempt a circulation for a patron', 'OpenILS::OpenAPI::Controller::patron', 'checkout_item', 'eg_auth_token param.userid req.json')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrievePatronCircs','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('requestPatronCirc','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrievePatronCircs','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrievePatronCirc', '/patron/:userid/checkout/:id', 'get', 'Retrieve one circulation for the specified user', 'open-ils.actor', 'open-ils.actor.user.transaction.fleshed.retrieve', 'eg_auth_token param.id'),
+ ('renewPatronCirc', '/patron/:userid/checkout/:id', 'put', 'Renew one circulation for the specified user', 'OpenILS::OpenAPI::Controller::patron', 'renew_circ', 'eg_auth_token param.id param.userid'),
+ ('checkinPatronCirc', '/patron/:userid/checkout/:id', 'delete', 'Check in one circulation for the specified user', 'OpenILS::OpenAPI::Controller::patron', 'checkin_circ', 'eg_auth_token param.id param.userid')
+ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES
+ ('retrievePatronCirc','userid','path','integer',TRUE),
+ ('retrievePatronCirc','id','path','integer',TRUE),
+ ('renewPatronCirc','userid','path','integer',TRUE),
+ ('renewPatronCirc','id','path','integer',TRUE),
+ ('checkinPatronCirc','userid','path','integer',TRUE),
+ ('checkinPatronCirc','id','path','integer',TRUE)
+ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronATEvents',
+ '/patron/:userid/triggered_events',
+ 'get',
+ 'Retrieve a list of A/T events for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'usr_at_events',
+ 'eg_auth_token param.userid param.limit param.offset param.before param.after every_param.hook'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value,required) VALUES
+ ('patronATEvents','userid','path','integer',NULL,NULL,TRUE),
+ ('patronATEvents','limit','query','integer',NULL,'100',FALSE),
+ ('patronATEvents','offset','query','integer',NULL,'0',FALSE),
+ ('patronATEvents','before','query','string','date-time',NULL,FALSE),
+ ('patronATEvents','after','query','string','date-time',NULL,FALSE),
+ ('patronATEvents','hook','query','string',NULL,NULL,FALSE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('patronATEvents','array','integer') ON CONFLICT DO NOTHING; -- array of ausp ids
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronATEvent',
+ '/patron/:userid/triggered_event/:eventid',
+ 'get',
+ 'Retrieve one penalty for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'usr_at_events',
+ 'eg_auth_token param.userid "1" "0" "" "" "" param.eventid'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronATEvent','eventid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronATEvent','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronActivePenalties',
+ '/patron/:userid/standing_penalties',
+ 'get',
+ 'Retrieve all penalty details for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'standing_penalties',
+ 'eg_auth_token param.userid'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronActivePenalties','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('patronActivePenalties','array','integer') ON CONFLICT DO NOTHING; -- array of ausp ids
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronPenalty',
+ '/patron/:userid/standing_penalty/:penaltyid',
+ 'get',
+ 'Retrieve one penalty for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'standing_penalties',
+ 'eg_auth_token param.userid "0" param.penaltyid'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronPenalty','penaltyid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronPenalty','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronActiveMessages',
+ '/patron/:userid/messages',
+ 'get',
+ 'Retrieve all active message ids for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'usr_messages',
+ 'eg_auth_token param.userid "0"'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronActiveMessages','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('patronActiveMessages','array','integer') ON CONFLICT DO NOTHING; -- array of aum ids
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronMessage',
+ '/patron/:userid/message/:msgid',
+ 'get',
+ 'Retrieve one message for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'usr_messages',
+ 'eg_auth_token param.userid "0" param.msgid'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessage','msgid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessage','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('patronMessage','aum') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronMessageUpdate',
+ '/patron/:userid/message/:msgid',
+ 'patch',
+ 'Update one message for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'update_usr_message',
+ 'eg_auth_token eg_user_id param.userid param.msgid req.json'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessageUpdate','msgid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessageUpdate','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('patronMessageUpdate','aum') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronMessageArchive',
+ '/patron/:userid/message/:msgid',
+ 'delete',
+ 'Archive one message for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'archive_usr_message',
+ 'eg_auth_token eg_user_id param.userid param.msgid'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessageArchive','msgid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessageArchive','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('patronMessageArchive','boolean') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronActivityLog',
+ '/patron/:userid/activity',
+ 'get',
+ 'Retrieve patron activity (authen, authz, etc)',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'usr_activity',
+ 'eg_auth_token param.userid param.maxage param.limit param.offset param.sort'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value,required) VALUES
+ ('patronActivityLog','userid','path','integer',NULL,NULL,TRUE),
+ ('patronActivityLog','limit','query','integer',NULL,'100',FALSE),
+ ('patronActivityLog','offset','query','integer',NULL,'0',FALSE),
+ ('patronActivityLog','sort','query','string',NULL,'desc',FALSE),
+ ('patronActivityLog','maxage','query','string','date-time',NULL,FALSE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('patronActivityLog','array','object') ON CONFLICT DO NOTHING;
+
+------- collections
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'collectionsPatronsOfInterest',
+ '/collections/:shortname/users_of_interest',
+ 'get',
+ 'List of patrons to consider for collections based on the search criteria provided.',
+ 'open-ils.collections',
+ 'open-ils.collections.users_of_interest.retrieve',
+ 'eg_auth_token param.fine_age param.fine_amount param.shortname'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,required) VALUES
+ ('collectionsPatronsOfInterest','shortname','path','string',NULL,TRUE),
+ ('collectionsPatronsOfInterest','fine_age','query','integer',NULL,TRUE),
+ ('collectionsPatronsOfInterest','fine_amount','query','string','money',TRUE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items,validate) VALUES ('collectionsPatronsOfInterest','array','object',FALSE) ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'collectionsPatronsOfInterestWarning',
+ '/collections/:shortname/users_of_interest/warning',
+ 'get',
+ 'List of patrons with the PATRON_EXCEEDS_COLLECTIONS_WARNING penalty to consider for collections based on the search criteria provided.',
+ 'open-ils.collections',
+ 'open-ils.collections.users_of_interest.warning_penalty.retrieve',
+ 'eg_auth_token param.shortname param.min_age param.max_age'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,required) VALUES
+ ('collectionsPatronsOfInterestWarning','shortname','path','string',NULL,TRUE),
+ ('collectionsPatronsOfInterestWarning','min_age','query','string','date-time',FALSE),
+ ('collectionsPatronsOfInterestWarning','max_age','query','string','date-time',FALSE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items,validate) VALUES ('collectionsPatronsOfInterestWarning','array','object',FALSE) ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'collectionsGetPatronDetail',
+ '/patron/:usrid/collections/:shortname',
+ 'get',
+ 'Get collections-related transaction details for a patron.',
+ 'open-ils.collections',
+ 'open-ils.collections.user_transaction_details.retrieve',
+ 'eg_auth_token param.start param.end param.shortname every_param.usrid'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,required) VALUES
+ ('collectionsGetPatronDetail','usrid','path','integer',NULL,TRUE),
+ ('collectionsGetPatronDetail','shortname','path','string',NULL,TRUE),
+ ('collectionsGetPatronDetail','start','query','string','date-time',TRUE),
+ ('collectionsGetPatronDetail','end','query','string','date-time',TRUE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items,validate) VALUES ('collectionsGetPatronDetail','array','object',FALSE) ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'collectionsPutPatronInCollections',
+ '/patron/:usrid/collections/:shortname',
+ 'post',
+ 'Put patron into collections.',
+ 'open-ils.collections',
+ 'open-ils.collections.put_into_collections',
+ 'eg_auth_token param.usrid param.shortname param.fee param.note'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,required) VALUES
+ ('collectionsPutPatronInCollections','usrid','path','integer',NULL,TRUE),
+ ('collectionsPutPatronInCollections','shortname','path','string',NULL,TRUE),
+ ('collectionsPutPatronInCollections','fee','query','string','money',FALSE),
+ ('collectionsPutPatronInCollections','note','query','string',NULL,FALSE)
+ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'collectionsRemovePatronFromCollections',
+ '/patron/:usrid/collections/:shortname',
+ 'delete',
+ 'Remove patron from collections.',
+ 'open-ils.collections',
+ 'open-ils.collections.remove_from_collections',
+ 'eg_auth_token param.usrid param.shortname'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES
+ ('collectionsRemovePatronFromCollections','usrid','path','integer',TRUE),
+ ('collectionsRemovePatronFromCollections','shortname','path','string',TRUE)
+ON CONFLICT DO NOTHING;
+
+
+------- courses
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('activeCourses', '/courses', 'get', 'Retrieve all courses used for course material reservation', 'OpenILS::OpenAPI::Controller::course', 'get_active_courses', 'eg_auth_token every_param.org')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES ('activeCourses','org','query','integer') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('activeCourses','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('activeRoles', '/courses/public_role_users', 'get', 'Retrieve all public roles used for courses', 'OpenILS::OpenAPI::Controller::course', 'get_all_course_public_roles', 'eg_auth_token every_param.org')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES ('activeRoles','org','query','integer') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('activeRoles','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrieveCourse', '/course/:id', 'get', 'Retrieve one detailed course', 'OpenILS::OpenAPI::Controller::course', 'get_course_detail', 'eg_auth_token param.id')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveCourse','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveCourse','acmc') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrieveCourseMaterials', '/course/:id/materials', 'get', 'Retrieve detailed materials for one course', 'OpenILS::OpenAPI::Controller::course', 'get_course_materials', 'param.id')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveCourseMaterials','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveCourseMaterials','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrieveCourseUsers', '/course/:id/public_role_users', 'get', 'Retrieve detailed user list for one course', 'open-ils.courses', 'open-ils.courses.course_users.retrieve', 'param.id')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveCourseUsers','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveCourseUsers','array','object') ON CONFLICT DO NOTHING;
+
+--------- put likely stock endpoints into sets --------
+
+INSERT INTO openapi.endpoint_set_endpoint_map (endpoint, endpoint_set)
+ SELECT e.operation_id, s.name FROM openapi.endpoint e JOIN openapi.endpoint_set s ON (e.path LIKE '/'||RTRIM(s.name,'s')||'%')
+ON CONFLICT DO NOTHING;
+
+-- Check that all endpoints are in sets -- should return 0 rows
+SELECT * FROM openapi.endpoint e WHERE NOT EXISTS (SELECT 1 FROM openapi.endpoint_set_endpoint_map WHERE endpoint = e.operation_id);
+
+-- Global Fieldmapper property-filtering org and user settings
+INSERT INTO config.settings_group (name,label) VALUES ('openapi','OpenAPI data access control');
+
+INSERT INTO config.org_unit_setting_type (name,label,grp) VALUES ('REST.api.blacklist_properties','Globally filtered Fieldmapper properties','openapi');
+INSERT INTO config.org_unit_setting_type (name,label,grp) VALUES ('REST.api.whitelist_properties','Globally whitelisted Fieldmapper properties','openapi');
+
+UPDATE config.org_unit_setting_type
+ SET update_perm = (SELECT id FROM permission.perm_list WHERE code = 'ADMIN_OPENAPI' LIMIT 1)
+ WHERE name IN ('REST.api.blacklist_properties','REST.api.whitelist_properties');
+
+INSERT INTO config.usr_setting_type (name,label,grp) VALUES ('REST.api.whitelist_properties','Globally whitelisted Fieldmapper properties','openapi');
+INSERT INTO config.usr_setting_type (name,label,grp) VALUES ('REST.api.blacklist_properties','Globally filtered Fieldmapper properties','openapi');
+
+
+/* -- Some extra example permission setup, to allow (basically) readonly patron retrieve
+
+INSERT INTO permission.perm_list (code,description) VALUES ('REST.api.patrons.detail.read','Permission meant to facilitate read-only patron-related API access');
+INSERT INTO openapi.perm_set (name) values ('Patron Detail, Readonly');
+INSERT INTO openapi.perm_set_perm_map (perm_set,perm) SELECT s.id, p.id FROM openapi.perm_set s, permission.perm_list p WHERE p.code IN ('REST.api', 'REST.api.patrons.detail.read') AND s.name='Patron RO';
+INSERT INTO openapi.endpoint_perm_set_map (endpoint,perm_set) SELECT 'retrievePatronProfile', s.id FROM openapi.perm_set s WHERE s.name='Patron RO';
+
+*/
+COMMIT;
+
+-- Update auditor tables to catch changes to source tables.
+-- Can be removed/skipped if there were no schema changes.
+SELECT auditor.update_auditors();
-----------------------------------------------------------------------
Summary of changes:
.../3.14.4-3.15.0-upgrade-db.sql} | 1242 +++++++++++++++++++-
1 file changed, 1241 insertions(+), 1 deletion(-)
copy Open-ILS/src/sql/Pg/{upgrade/1468.schema.openapi.sql => version-upgrade/3.14.4-3.15.0-upgrade-db.sql} (63%)
mode change 100755 => 100644
hooks/post-receive
--
Evergreen ILS
1
0

[GIT] Evergreen ILS branch main updated. 06fabb27ef20708be766e68284535cf60afd78f2
by Git User 15 Apr '25
by Git User 15 Apr '25
15 Apr '25
This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".
The branch, main has been updated
via 06fabb27ef20708be766e68284535cf60afd78f2 (commit)
via 75c301f7f8ef9f87e75903a6da1ec85e16876ee6 (commit)
via 70e4330d088144db4de6f15775dc4c10b16d8a36 (commit)
from ed66e0b6f1e3980c660af7f429ba7549f720464f (commit)
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
- Log -----------------------------------------------------------------
commit 06fabb27ef20708be766e68284535cf60afd78f2
Author: Michele Morgan <mmorgan(a)noblenet.org>
Date: Tue Apr 15 16:52:22 2025 -0400
Forward-port 3.14.4-3.15.0 upgrade script
Signed-off-by: Michele Morgan <mmorgan(a)noblenet.org>
diff --git a/Open-ILS/src/sql/Pg/version-upgrade/3.14.4-3.15.0-upgrade-db.sql b/Open-ILS/src/sql/Pg/version-upgrade/3.14.4-3.15.0-upgrade-db.sql
new file mode 100644
index 0000000000..2dac5d2ca9
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/version-upgrade/3.14.4-3.15.0-upgrade-db.sql
@@ -0,0 +1,2712 @@
+--Upgrade Script for 3.14.4 to 3.15.0
+\set eg_version '''3.15.0'''
+BEGIN;
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('3.15.0', :eg_version);
+
+SELECT evergreen.upgrade_deps_block_check('1450', :eg_version);
+
+INSERT into config.workstation_setting_type
+ (name, grp, label, description, datatype)
+VALUES (
+ 'eg.admin.keyboard_shortcuts.disable_single',
+ 'gui',
+ oils_i18n_gettext('eg.admin.keyboard_shortcuts.disable_single',
+ 'Staff Client: disable single-letter keyboard shortcuts',
+ 'coust', 'label'),
+ oils_i18n_gettext('eg.admin.keyboard_shortcuts.disable_single',
+ 'Disables single-letter keyboard shortcuts if set to true. Screen reader users should set this to true to avoid interference with standard keyboard shortcuts.',
+ 'coust', 'description'),
+ 'bool'
+);
+
+
+SELECT evergreen.upgrade_deps_block_check('1452', :eg_version);
+
+INSERT INTO config.org_unit_setting_type
+ (name, label, description, grp, datatype)
+VALUES (
+ 'ui.staff.place_holds_for_recent_patrons',
+ oils_i18n_gettext(
+ 'ui.staff.place_holds_for_recent_patrons',
+ 'Place holds for recent patrons',
+ 'coust',
+ 'label'
+ ),
+ oils_i18n_gettext(
+ 'ui.staff.place_holds_for_recent_patrons',
+ 'Loading a patron in the place holds interface designates them as recent. ' ||
+ 'Show the interface to load recent patrons when placing holds.',
+ 'coust',
+ 'description'
+ ),
+ 'gui',
+ 'bool'
+);
+
+
+SELECT evergreen.upgrade_deps_block_check('1453', :eg_version);
+
+INSERT INTO permission.perm_list ( id, code, description ) SELECT DISTINCT
+ 676,
+ 'UPDATE_TOP_OF_QUEUE',
+ oils_i18n_gettext(676,
+ 'Allow setting and unsetting hold from top of hold queue (cut in line)', 'ppl', 'description'
+ )
+ FROM permission.perm_list
+ WHERE NOT EXISTS (SELECT 1 FROM permission.perm_list WHERE code = 'UPDATE_TOP_OF_QUEUE');
+
+
+--Assign permission to any perm groups with UPDATE_HOLD_REQUEST_TIME
+WITH perms_to_add AS
+ (SELECT id FROM
+ permission.perm_list
+ WHERE code IN ('UPDATE_TOP_OF_QUEUE'))
+INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
+ SELECT grp, perms_to_add.id as perm, depth, grantable
+ FROM perms_to_add,
+ permission.grp_perm_map
+
+ --- Don't add the permissions if they have already been assigned
+ WHERE grp NOT IN
+ (SELECT DISTINCT grp FROM permission.grp_perm_map
+ INNER JOIN perms_to_add ON perm=perms_to_add.id)
+
+ --- we're going to match the depth of their existing perm
+ AND perm = (
+ SELECT id
+ FROM permission.perm_list
+ WHERE code = 'UPDATE_HOLD_REQUEST_TIME'
+ );
+
+-- Add missing FROM clause entry to asset.opac_lasso_record_copy_count
+
+
+SELECT evergreen.upgrade_deps_block_check('1455', :eg_version);
+
+CREATE OR REPLACE FUNCTION asset.opac_lasso_record_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
+DECLARE
+ ans RECORD;
+ trans INT;
+BEGIN
+ SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
+
+ FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
+ RETURN QUERY
+ WITH org_list AS (SELECT ARRAY_AGG(id)::BIGINT[] AS orgs FROM actor.org_unit_descendants(ans.id) x),
+ available_statuses AS (SELECT ARRAY_AGG(id) AS ids FROM config.copy_status WHERE is_available),
+ mask AS (SELECT c_attrs FROM asset.patron_default_visibility_mask() x)
+ SELECT -1,
+ ans.id,
+ COUNT( av.id ),
+ SUM( (cp.status = ANY (available_statuses.ids))::INT ),
+ COUNT( av.id ),
+ trans
+ FROM mask,
+ org_list,
+ available_statuses,
+ asset.copy_vis_attr_cache av
+ JOIN asset.copy cp ON (cp.id = av.target_copy AND av.record = rid)
+ WHERE cp.circ_lib = ANY (org_list.orgs) AND av.vis_attr_vector @@ mask.c_attrs::query_int
+ GROUP BY 1,2,6;
+
+ IF NOT FOUND THEN
+ RETURN QUERY SELECT -1, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
+ END IF;
+
+ END LOOP;
+
+ RETURN;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+
+SELECT evergreen.upgrade_deps_block_check('1456', :eg_version);
+
+INSERT into config.workstation_setting_type
+ (name, grp, label, description, datatype)
+VALUES (
+ 'ui.staff.grid.density',
+ 'gui',
+ oils_i18n_gettext('ui.staff.grid.density',
+ 'Grid UI density',
+ 'coust', 'label'),
+ oils_i18n_gettext('ui.staff.grid.density',
+ 'Whitespace around table cells in staff UI data grids. Default is "standard".',
+ 'coust', 'description'),
+ 'string'
+);
+
+
+-- Move OPAC alert banner feature from config file to a library setting
+
+SELECT evergreen.upgrade_deps_block_check('1458', :eg_version);
+
+INSERT into config.org_unit_setting_type
+ (name, grp, label, description, datatype)
+VALUES (
+ 'opac.alert_banner_show',
+ 'opac',
+ oils_i18n_gettext('opac.alert_banner_show',
+ 'OPAC Alert Banner: Display',
+ 'coust', 'label'),
+ oils_i18n_gettext('opac.alert_message_show',
+ 'Show an alert banner in the OPAC. Default is false.',
+ 'coust', 'description'),
+ 'bool'
+);
+
+INSERT into config.org_unit_setting_type
+ (name, grp, label, description, datatype)
+VALUES (
+ 'opac.alert_banner_type',
+ 'opac',
+ oils_i18n_gettext('opac.alert_banner_type',
+ 'OPAC Alert Banner: Type',
+ 'coust', 'label'),
+ oils_i18n_gettext('opac.alert_message_type',
+ 'Determine the display of the banner. Options are: success, info, warning, danger.',
+ 'coust', 'description'),
+ 'string'
+);
+
+INSERT into config.org_unit_setting_type
+ (name, grp, label, description, datatype)
+VALUES (
+ 'opac.alert_banner_text',
+ 'opac',
+ oils_i18n_gettext('opac.alert_banner_text',
+ 'OPAC Alert Banner: Text',
+ 'coust', 'label'),
+ oils_i18n_gettext('opac.alert_message_text',
+ 'Text that will display in the alert banner.',
+ 'coust', 'description'),
+ 'string'
+);
+
+
+-- Move Google Analytics settings from config.tt2 to library settings
+
+SELECT evergreen.upgrade_deps_block_check('1459', :eg_version);
+
+INSERT into config.org_unit_setting_type
+ (name, grp, label, description, datatype)
+VALUES (
+ 'opac.google_analytics_enable',
+ 'opac',
+ oils_i18n_gettext('opac.google_analytics_enable',
+ 'Google Analytics: Enable',
+ 'coust', 'label'),
+ oils_i18n_gettext('opac.alert_message_show',
+ 'Enable Google Analytics in the OPAC. Default is false.',
+ 'coust', 'description'),
+ 'bool'
+);
+
+INSERT into config.org_unit_setting_type
+ (name, grp, label, description, datatype)
+VALUES (
+ 'opac.google_analytics_code',
+ 'opac',
+ oils_i18n_gettext('opac.google_analytics_code',
+ 'Google Analytics: Code',
+ 'coust', 'label'),
+ oils_i18n_gettext('opac.google_analytics_code',
+ 'Account code provided by Google. (Example: G-GVGQ11X12)',
+ 'coust', 'description'),
+ 'string'
+);
+
+
+SELECT evergreen.upgrade_deps_block_check('1460', :eg_version); -- JBoyer
+
+-- Note: the value will not be consistent from system to system. It's an opaque key that has no meaning of its own,
+-- if the value cached in the client does not match or is missing, it clears some cached values and then saves the current value.
+INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
+ 'staff.client_cache_key',
+ oils_i18n_gettext(
+ 'staff.client_cache_key',
+ 'Change this value to force staff clients to clear some cached values',
+ 'cgf',
+ 'label'
+ ),
+ md5(random()::text),
+ TRUE
+);
+
+
+SELECT evergreen.upgrade_deps_block_check('1461', :eg_version);
+
+-- for searching e.g. "111-111-1111"
+CREATE INDEX actor_usr_setting_phone_values_idx
+ ON actor.usr_setting (evergreen.lowercase(value))
+ WHERE name IN ('opac.default_phone', 'opac.default_sms_notify');
+
+-- for searching e.g. "1111111111"
+CREATE INDEX actor_usr_setting_phone_values_numeric_idx
+ ON actor.usr_setting (evergreen.lowercase(REGEXP_REPLACE(value, '[^0-9]', '', 'g')))
+ WHERE name IN ('opac.default_phone', 'opac.default_sms_notify');
+
+
+SELECT evergreen.upgrade_deps_block_check('1462', :eg_version);
+
+INSERT INTO acq.edi_attr (key, label) VALUES
+ ('LINEITEM_SEQUENTIAL_ID',
+ oils_i18n_gettext('LINEITEM_SEQUENTIAL_ID',
+ 'Lineitems Are Enumerated Sequentially', 'aea', 'label'));
+
+
+SELECT evergreen.upgrade_deps_block_check('1463', :eg_version);
+
+INSERT INTO config.org_unit_setting_type
+ (grp, name, datatype, label, description)
+VALUES (
+ 'gui',
+ 'ui.cat.volume_copy_editor.template_bar.show_save_template', 'bool',
+ oils_i18n_gettext(
+ 'ui.cat.volume_copy_editor.template_bar.show_save_template',
+ 'Show "Save Template" in Holdings Editor',
+ 'coust',
+ 'label'
+ ),
+ oils_i18n_gettext(
+ 'ui.cat.volume_copy_editor.template_bar.show_save_template',
+ 'Displays the "Save Template" button for the template bar in the Volume/Copy/Holdings Editor. By default, this is only displayed when working with templates from the Admin interface.',
+ 'coust',
+ 'description'
+ )
+);
+
+INSERT INTO config.org_unit_setting_type
+ (grp, name, datatype, label, description)
+VALUES (
+ 'gui',
+ 'ui.cat.volume_copy_editor.hide_template_bar', 'bool',
+ oils_i18n_gettext(
+ 'ui.cat.volume_copy_editor.hide_template_bar',
+ 'Hide the entire template bar in Holdings Editor',
+ 'coust',
+ 'label'
+ ),
+ oils_i18n_gettext(
+ 'ui.cat.volume_copy_editor.hide_template_bar',
+ 'Hides the template bar in the Volume/Copy/Holdings Editor. By default, the template bar is displayed in this interface.',
+ 'coust',
+ 'description'
+ )
+);
+
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+ 'eg.grid.cat.volcopy.template_grid', 'gui', 'object',
+ oils_i18n_gettext(
+ 'eg.grid.cat.volcopy.template_grid',
+ 'Holdings Template Grid Settings',
+ 'cwst', 'label'
+ )
+);
+
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+ 'eg.grid.holdings.copy_tags.tag_map_list', 'gui', 'object',
+ oils_i18n_gettext(
+ 'eg.grid.holdings.copy_tags.tag_map_list',
+ 'Copy Tag Maps Template Grid Settings',
+ 'cwst', 'label'
+ )
+);
+
+
+SELECT evergreen.upgrade_deps_block_check('1464', :eg_version);
+
+-- If you want to know how many items are available in a particular library group,
+-- you can't easily sum the results of, say, asset.staff_lasso_record_copy_count,
+-- since library groups can theoretically include descendants of other org units
+-- in the library group (for example, the group could include a system and a branch
+-- within that same system), which means that certain items would be counted twice.
+-- The following functions address that problem by providing deduplicated sums that
+-- only count each item once.
+
+CREATE OR REPLACE FUNCTION asset.staff_lasso_record_copy_count_sum(lasso_id INT, record_id BIGINT)
+ RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT, library_group INT) AS $$
+ BEGIN
+ IF (lasso_id IS NULL) THEN RETURN; END IF;
+ IF (record_id IS NULL) THEN RETURN; END IF;
+ RETURN QUERY SELECT
+ -1,
+ -1,
+ COUNT(cp.id),
+ SUM( CASE WHEN cp.status IN (SELECT id FROM config.copy_status WHERE holdable AND is_available) THEN 1 ELSE 0 END ),
+ SUM( CASE WHEN cl.opac_visible AND cp.opac_visible THEN 1 ELSE 0 END),
+ 0,
+ lasso_id
+ FROM ( SELECT DISTINCT descendants.id FROM actor.org_lasso_map aolmp JOIN LATERAL actor.org_unit_descendants(aolmp.org_unit) AS descendants ON TRUE WHERE aolmp.lasso = lasso_id) d
+ JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
+ JOIN asset.copy_location cl ON (cp.location = cl.id AND NOT cl.deleted)
+ JOIN asset.call_number cn ON (cn.record = record_id AND cn.id = cp.call_number AND NOT cn.deleted);
+ END;
+$$ LANGUAGE PLPGSQL STABLE ROWS 1;
+
+CREATE OR REPLACE FUNCTION asset.opac_lasso_record_copy_count_sum(lasso_id INT, record_id BIGINT)
+ RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT, library_group INT) AS $$
+ BEGIN
+ IF (lasso_id IS NULL) THEN RETURN; END IF;
+ IF (record_id IS NULL) THEN RETURN; END IF;
+
+ RETURN QUERY SELECT
+ -1,
+ -1,
+ COUNT(cp.id),
+ SUM( CASE WHEN cp.status IN (SELECT id FROM config.copy_status WHERE holdable AND is_available) THEN 1 ELSE 0 END ),
+ SUM( CASE WHEN cl.opac_visible AND cp.opac_visible THEN 1 ELSE 0 END),
+ 0,
+ lasso_id
+ FROM ( SELECT DISTINCT descendants.id FROM actor.org_lasso_map aolmp JOIN LATERAL actor.org_unit_descendants(aolmp.org_unit) AS descendants ON TRUE WHERE aolmp.lasso = lasso_id) d
+ JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
+ JOIN asset.copy_location cl ON (cp.location = cl.id AND NOT cl.deleted)
+ JOIN asset.call_number cn ON (cn.record = record_id AND cn.id = cp.call_number AND NOT cn.deleted)
+ JOIN asset.copy_vis_attr_cache av ON (cp.id = av.target_copy AND av.record = record_id)
+ JOIN LATERAL (SELECT c_attrs FROM asset.patron_default_visibility_mask()) AS mask ON TRUE
+ WHERE av.vis_attr_vector @@ mask.c_attrs::query_int;
+ END;
+$$ LANGUAGE PLPGSQL STABLE ROWS 1;
+
+CREATE OR REPLACE FUNCTION asset.staff_lasso_metarecord_copy_count_sum(lasso_id INT, metarecord_id BIGINT)
+ RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT, library_group INT) AS $$
+ SELECT (
+ -1,
+ -1,
+ SUM(sums.visible)::bigint,
+ SUM(sums.available)::bigint,
+ SUM(sums.unshadow)::bigint,
+ MIN(sums.transcendant),
+ lasso_id
+ ) FROM metabib.metarecord_source_map mmsm
+ JOIN LATERAL (SELECT visible, available, unshadow, transcendant FROM asset.staff_lasso_record_copy_count_sum(lasso_id, mmsm.source)) sums ON TRUE
+ WHERE mmsm.metarecord = metarecord_id;
+$$ LANGUAGE SQL STABLE ROWS 1;
+
+CREATE OR REPLACE FUNCTION asset.opac_lasso_metarecord_copy_count_sum(lasso_id INT, metarecord_id BIGINT)
+ RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT, library_group INT) AS $$
+ SELECT (
+ -1,
+ -1,
+ SUM(sums.visible)::bigint,
+ SUM(sums.available)::bigint,
+ SUM(sums.unshadow)::bigint,
+ MIN(sums.transcendant),
+ lasso_id
+ ) FROM metabib.metarecord_source_map mmsm
+ JOIN LATERAL (SELECT visible, available, unshadow, transcendant FROM asset.opac_lasso_record_copy_count_sum(lasso_id, mmsm.source)) sums ON TRUE
+ WHERE mmsm.metarecord = metarecord_id;
+$$ LANGUAGE SQL STABLE ROWS 1;
+
+
+CREATE OR REPLACE FUNCTION asset.copy_org_ids(org_units INT[], depth INT, library_groups INT[])
+RETURNS TABLE (id INT)
+AS $$
+DECLARE
+ ancestor INT;
+BEGIN
+ RETURN QUERY SELECT org_unit FROM actor.org_lasso_map WHERE lasso = ANY(library_groups);
+ FOR ancestor IN SELECT unnest(org_units)
+ LOOP
+ RETURN QUERY
+ SELECT d.id
+ FROM actor.org_unit_descendants(ancestor, depth) d;
+ END LOOP;
+ RETURN;
+END;
+$$ LANGUAGE plpgsql;
+
+
+CREATE OR REPLACE FUNCTION asset.staff_copy_total(rec_id INT, org_units INT[], depth INT, library_groups INT[])
+RETURNS INT AS $$
+ SELECT COUNT(cp.id) total
+ FROM asset.copy cp
+ INNER JOIN asset.call_number cn ON (cn.id = cp.call_number AND NOT cn.deleted AND cn.record = rec_id)
+ INNER JOIN asset.copy_location cl ON (cp.location = cl.id AND NOT cl.deleted)
+ WHERE cp.circ_lib = ANY (SELECT asset.copy_org_ids(org_units, depth, library_groups))
+ AND NOT cp.deleted;
+$$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION asset.opac_copy_total(rec_id INT, org_units INT[], depth INT, library_groups INT[])
+RETURNS INT AS $$
+ SELECT COUNT(cp.id) total
+ FROM asset.copy cp
+ INNER JOIN asset.call_number cn ON (cn.id = cp.call_number AND NOT cn.deleted AND cn.record = rec_id)
+ INNER JOIN asset.copy_location cl ON (cp.location = cl.id AND NOT cl.deleted)
+ INNER JOIN asset.copy_vis_attr_cache av ON (cp.id = av.target_copy AND av.record = rec_id)
+ JOIN LATERAL (SELECT c_attrs FROM asset.patron_default_visibility_mask()) AS mask ON TRUE
+ WHERE av.vis_attr_vector @@ mask.c_attrs::query_int
+ AND cp.circ_lib = ANY (SELECT asset.copy_org_ids(org_units, depth, library_groups))
+ AND NOT cp.deleted;
+$$ LANGUAGE SQL;
+
+-- We are adding another argument, which means that we must delete functions with the old signature
+DROP FUNCTION IF EXISTS unapi.holdings_xml(
+ bid BIGINT,
+ ouid INT,
+ org TEXT,
+ depth INT,
+ includes TEXT[],
+ slimit HSTORE,
+ soffset HSTORE,
+ include_xmlns BOOL,
+ pref_lib INT);
+
+CREATE OR REPLACE FUNCTION unapi.holdings_xml (
+ bid BIGINT,
+ ouid INT,
+ org TEXT,
+ depth INT DEFAULT NULL,
+ includes TEXT[] DEFAULT NULL::TEXT[],
+ slimit HSTORE DEFAULT NULL,
+ soffset HSTORE DEFAULT NULL,
+ include_xmlns BOOL DEFAULT TRUE,
+ pref_lib INT DEFAULT NULL,
+ library_group INT DEFAULT NULL
+)
+RETURNS XML AS $F$
+ SELECT XMLELEMENT(
+ name holdings,
+ XMLATTRIBUTES(
+ CASE WHEN $8 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+ CASE WHEN ('bre' = ANY ($5)) THEN 'tag:open-ils.org:U2@bre/' || $1 || '/' || $3 ELSE NULL END AS id,
+ (SELECT record_has_holdable_copy FROM asset.record_has_holdable_copy($1)) AS has_holdable
+ ),
+ XMLELEMENT(
+ name counts,
+ (SELECT XMLAGG(XMLELEMENT::XML) FROM (
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('public' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.opac_ou_record_copy_count($2, $1)
+ UNION
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('staff' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.staff_ou_record_copy_count($2, $1)
+ UNION
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('pref_lib' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.opac_ou_record_copy_count($9, $1)
+ UNION
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('public' as type, depth, library_group, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.opac_lasso_record_copy_count_sum(library_group, $1)
+ UNION
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('staff' as type, depth, library_group, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.staff_lasso_record_copy_count_sum(library_group, $1)
+ ORDER BY 1
+ )x)
+ ),
+ CASE
+ WHEN ('bmp' = ANY ($5)) THEN
+ XMLELEMENT(
+ name monograph_parts,
+ (SELECT XMLAGG(bmp) FROM (
+ SELECT unapi.bmp( id, 'xml', 'monograph_part', array_remove( array_remove($5,'bre'), 'holdings_xml'), $3, $4, $6, $7, FALSE)
+ FROM biblio.monograph_part
+ WHERE NOT deleted AND record = $1
+ )x)
+ )
+ ELSE NULL
+ END,
+ XMLELEMENT(
+ name volumes,
+ (SELECT XMLAGG(acn ORDER BY rank, name, label_sortkey) FROM (
+ -- Physical copies
+ SELECT unapi.acn(y.id,'xml','volume',array_remove( array_remove($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE), y.rank, name, label_sortkey
+ FROM evergreen.ranked_volumes($1, $2, $4, $6, $7, $9, $5) AS y
+ UNION ALL
+ -- Located URIs
+ SELECT unapi.acn(uris.id,'xml','volume',array_remove( array_remove($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE), uris.rank, name, label_sortkey
+ FROM evergreen.located_uris($1, $2, $9) AS uris
+ )x)
+ ),
+ CASE WHEN ('ssub' = ANY ($5)) THEN
+ XMLELEMENT(
+ name subscriptions,
+ (SELECT XMLAGG(ssub) FROM (
+ SELECT unapi.ssub(id,'xml','subscription','{}'::TEXT[], $3, $4, $6, $7, FALSE)
+ FROM serial.subscription
+ WHERE record_entry = $1
+ )x)
+ )
+ ELSE NULL END,
+ CASE WHEN ('acp' = ANY ($5)) THEN
+ XMLELEMENT(
+ name foreign_copies,
+ (SELECT XMLAGG(acp) FROM (
+ SELECT unapi.acp(p.target_copy,'xml','copy',array_remove($5,'acp'), $3, $4, $6, $7, FALSE)
+ FROM biblio.peer_bib_copy_map p
+ JOIN asset.copy c ON (p.target_copy = c.id)
+ WHERE NOT c.deleted AND p.peer_record = $1
+ LIMIT ($6 -> 'acp')::INT
+ OFFSET ($7 -> 'acp')::INT
+ )x)
+ )
+ ELSE NULL END
+ );
+$F$ LANGUAGE SQL STABLE;
+
+DROP FUNCTION IF EXISTS unapi.bre (
+ obj_id BIGINT,
+ format TEXT,
+ ename TEXT,
+ includes TEXT[],
+ org TEXT,
+ depth INT,
+ slimit HSTORE,
+ soffset HSTORE,
+ include_xmlns BOOL,
+ pref_lib INT);
+
+CREATE OR REPLACE FUNCTION unapi.bre (
+ obj_id BIGINT,
+ format TEXT,
+ ename TEXT,
+ includes TEXT[],
+ org TEXT,
+ depth INT DEFAULT NULL,
+ slimit HSTORE DEFAULT NULL,
+ soffset HSTORE DEFAULT NULL,
+ include_xmlns BOOL DEFAULT TRUE,
+ pref_lib INT DEFAULT NULL,
+ library_group INT DEFAULT NULL
+)
+RETURNS XML AS $F$
+DECLARE
+ me biblio.record_entry%ROWTYPE;
+ layout unapi.bre_output_layout%ROWTYPE;
+ xfrm config.xml_transform%ROWTYPE;
+ ouid INT;
+ tmp_xml TEXT;
+ top_el TEXT;
+ output XML;
+ hxml XML;
+ axml XML;
+ source XML;
+BEGIN
+
+ IF org = '-' OR org IS NULL THEN
+ SELECT shortname INTO org FROM evergreen.org_top();
+ END IF;
+
+ SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
+
+ IF ouid IS NULL THEN
+ RETURN NULL::XML;
+ END IF;
+
+ IF format = 'holdings_xml' THEN -- the special case
+ output := unapi.holdings_xml( obj_id, ouid, org, depth, includes, slimit, soffset, include_xmlns, NULL, library_group);
+ RETURN output;
+ END IF;
+
+ SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
+
+ IF layout.name IS NULL THEN
+ RETURN NULL::XML;
+ END IF;
+
+ SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform;
+
+ SELECT * INTO me FROM biblio.record_entry WHERE id = obj_id;
+
+ -- grab bib_source, if any
+ IF ('cbs' = ANY (includes) AND me.source IS NOT NULL) THEN
+ source := unapi.cbs(me.source,NULL,NULL,NULL,NULL);
+ ELSE
+ source := NULL::XML;
+ END IF;
+
+ -- grab SVF if we need them
+ IF ('mra' = ANY (includes)) THEN
+ axml := unapi.mra(obj_id,NULL,NULL,NULL,NULL);
+ ELSE
+ axml := NULL::XML;
+ END IF;
+
+ -- grab holdings if we need them
+ IF ('holdings_xml' = ANY (includes)) THEN
+ hxml := unapi.holdings_xml(obj_id, ouid, org, depth, array_remove(includes,'holdings_xml'), slimit, soffset, include_xmlns, pref_lib, library_group);
+ ELSE
+ hxml := NULL::XML;
+ END IF;
+
+
+ -- generate our item node
+
+
+ IF format = 'marcxml' THEN
+ tmp_xml := me.marc;
+ IF tmp_xml !~ E'<marc:' THEN -- If we're not using the prefixed namespace in this record, then remove all declarations of it
+ tmp_xml := REGEXP_REPLACE(tmp_xml, ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g');
+ END IF;
+ ELSE
+ tmp_xml := oils_xslt_process(me.marc, xfrm.xslt)::XML;
+ END IF;
+
+ top_el := REGEXP_REPLACE(tmp_xml, E'^.*?<((?:\\S+:)?' || layout.holdings_element || ').*$', E'\\1');
+
+ IF source IS NOT NULL THEN
+ tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', source || '</' || top_el || E'>\\1');
+ END IF;
+
+ IF axml IS NOT NULL THEN
+ tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', axml || '</' || top_el || E'>\\1');
+ END IF;
+
+ IF hxml IS NOT NULL THEN -- XXX how do we configure the holdings position?
+ tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1');
+ END IF;
+
+ IF ('bre.unapi' = ANY (includes)) THEN
+ output := REGEXP_REPLACE(
+ tmp_xml,
+ '</' || top_el || '>(.*?)',
+ XMLELEMENT(
+ name abbr,
+ XMLATTRIBUTES(
+ 'http://www.w3.org/1999/xhtml' AS xmlns,
+ 'unapi-id' AS class,
+ 'tag:open-ils.org:U2@bre/' || obj_id || '/' || org AS title
+ )
+ )::TEXT || '</' || top_el || E'>\\1'
+ );
+ ELSE
+ output := tmp_xml;
+ END IF;
+
+ IF ('bre.extern' = ANY (includes)) THEN
+ output := REGEXP_REPLACE(
+ tmp_xml,
+ '</' || top_el || '>(.*?)',
+ XMLELEMENT(
+ name extern,
+ XMLATTRIBUTES(
+ 'http://open-ils.org/spec/biblio/v1' AS xmlns,
+ me.creator AS creator,
+ me.editor AS editor,
+ me.create_date AS create_date,
+ me.edit_date AS edit_date,
+ me.quality AS quality,
+ me.fingerprint AS fingerprint,
+ me.tcn_source AS tcn_source,
+ me.tcn_value AS tcn_value,
+ me.owner AS owner,
+ me.share_depth AS share_depth,
+ me.active AS active,
+ me.deleted AS deleted
+ )
+ )::TEXT || '</' || top_el || E'>\\1'
+ );
+ ELSE
+ output := tmp_xml;
+ END IF;
+
+ output := REGEXP_REPLACE(output::TEXT,E'>\\s+<','><','gs')::XML;
+ RETURN output;
+END;
+$F$ LANGUAGE PLPGSQL STABLE;
+
+DROP FUNCTION IF EXISTS unapi.biblio_record_entry_feed (
+ id_list BIGINT[],
+ format TEXT,
+ includes TEXT[],
+ org TEXT,
+ depth INT,
+ slimit HSTORE,
+ soffset HSTORE,
+ include_xmlns BOOL,
+ title TEXT,
+ description TEXT,
+ creator TEXT,
+ update_ts TEXT,
+ unapi_url TEXT,
+ header_xml XML,
+ pref_lib INT);
+CREATE OR REPLACE FUNCTION unapi.biblio_record_entry_feed (
+ id_list BIGINT[],
+ format TEXT,
+ includes TEXT[],
+ org TEXT,
+ depth INT DEFAULT NULL,
+ slimit HSTORE DEFAULT NULL,
+ soffset HSTORE DEFAULT NULL,
+ include_xmlns BOOL DEFAULT TRUE,
+ title TEXT DEFAULT NULL,
+ description TEXT DEFAULT NULL,
+ creator TEXT DEFAULT NULL,
+ update_ts TEXT DEFAULT NULL,
+ unapi_url TEXT DEFAULT NULL,
+ header_xml XML DEFAULT NULL,
+ pref_lib INT DEFAULT NULL,
+ library_group INT DEFAULT NULL
+ ) RETURNS XML AS $F$
+DECLARE
+ layout unapi.bre_output_layout%ROWTYPE;
+ transform config.xml_transform%ROWTYPE;
+ item_format TEXT;
+ tmp_xml TEXT;
+ xmlns_uri TEXT := 'http://open-ils.org/spec/feed-xml/v1';
+ ouid INT;
+ element_list TEXT[];
+BEGIN
+
+ IF org = '-' OR org IS NULL THEN
+ SELECT shortname INTO org FROM evergreen.org_top();
+ END IF;
+
+ SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
+ SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
+
+ IF layout.name IS NULL THEN
+ RETURN NULL::XML;
+ END IF;
+
+ SELECT * INTO transform FROM config.xml_transform WHERE name = layout.transform;
+ xmlns_uri := COALESCE(transform.namespace_uri,xmlns_uri);
+
+ -- Gather the bib xml
+ SELECT XMLAGG( unapi.bre(i, format, '', includes, org, depth, slimit, soffset, include_xmlns, pref_lib, library_group)) INTO tmp_xml FROM UNNEST( id_list ) i;
+
+ IF layout.title_element IS NOT NULL THEN
+ EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.title_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, title;
+ END IF;
+
+ IF layout.description_element IS NOT NULL THEN
+ EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.description_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, description;
+ END IF;
+
+ IF layout.creator_element IS NOT NULL THEN
+ EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.creator_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, creator;
+ END IF;
+
+ IF layout.update_ts_element IS NOT NULL THEN
+ EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.update_ts_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, update_ts;
+ END IF;
+
+ IF unapi_url IS NOT NULL THEN
+ EXECUTE $$SELECT XMLCONCAT( XMLELEMENT( name link, XMLATTRIBUTES( 'http://www.w3.org/1999/xhtml' AS xmlns, 'unapi-server' AS rel, $1 AS href, 'unapi' AS title)), $2)$$ INTO tmp_xml USING unapi_url, tmp_xml::XML;
+ END IF;
+
+ IF header_xml IS NOT NULL THEN tmp_xml := XMLCONCAT(header_xml,tmp_xml::XML); END IF;
+
+ element_list := regexp_split_to_array(layout.feed_top,E'\\.');
+ FOR i IN REVERSE ARRAY_UPPER(element_list, 1) .. 1 LOOP
+ EXECUTE 'SELECT XMLELEMENT( name '|| quote_ident(element_list[i]) ||', XMLATTRIBUTES( $1 AS xmlns), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML;
+ END LOOP;
+
+ RETURN tmp_xml::XML;
+END;
+$F$ LANGUAGE PLPGSQL STABLE;
+
+
+DROP FUNCTION IF EXISTS unapi.metabib_virtual_record_feed (
+ id_list BIGINT[],
+ format TEXT,
+ includes TEXT[],
+ org TEXT,
+ depth INT,
+ slimit HSTORE,
+ soffset HSTORE,
+ include_xmlns BOOL, title TEXT,
+ description TEXT,
+ creator TEXT,
+ update_ts TEXT,
+ unapi_url TEXT,
+ header_xml XML,
+ pref_lib INT);
+CREATE OR REPLACE FUNCTION unapi.metabib_virtual_record_feed (
+ id_list BIGINT[],
+ format TEXT,
+ includes TEXT[],
+ org TEXT,
+ depth INT DEFAULT NULL,
+ slimit HSTORE DEFAULT NULL,
+ soffset HSTORE DEFAULT NULL,
+ include_xmlns BOOL DEFAULT TRUE,
+ title TEXT DEFAULT NULL,
+ description TEXT DEFAULT NULL,
+ creator TEXT DEFAULT NULL,
+ update_ts TEXT DEFAULT NULL,
+ unapi_url TEXT DEFAULT NULL,
+ header_xml XML DEFAULT NULL,
+ pref_lib INT DEFAULT NULL,
+ library_group INT DEFAULT NULL ) RETURNS XML AS $F$
+DECLARE
+ layout unapi.bre_output_layout%ROWTYPE;
+ transform config.xml_transform%ROWTYPE;
+ item_format TEXT;
+ tmp_xml TEXT;
+ xmlns_uri TEXT := 'http://open-ils.org/spec/feed-xml/v1';
+ ouid INT;
+ element_list TEXT[];
+BEGIN
+
+ IF org = '-' OR org IS NULL THEN
+ SELECT shortname INTO org FROM evergreen.org_top();
+ END IF;
+
+ SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
+ SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
+
+ IF layout.name IS NULL THEN
+ RETURN NULL::XML;
+ END IF;
+
+ SELECT * INTO transform FROM config.xml_transform WHERE name = layout.transform;
+ xmlns_uri := COALESCE(transform.namespace_uri,xmlns_uri);
+
+ -- Gather the bib xml
+ SELECT XMLAGG( unapi.mmr(i, format, '', includes, org, depth, slimit, soffset, include_xmlns, pref_lib, library_group)) INTO tmp_xml FROM UNNEST( id_list ) i;
+
+ IF layout.title_element IS NOT NULL THEN
+ EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.title_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, title;
+ END IF;
+
+ IF layout.description_element IS NOT NULL THEN
+ EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.description_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, description;
+ END IF;
+
+ IF layout.creator_element IS NOT NULL THEN
+ EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.creator_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, creator;
+ END IF;
+
+ IF layout.update_ts_element IS NOT NULL THEN
+ EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.update_ts_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, update_ts;
+ END IF;
+
+ IF unapi_url IS NOT NULL THEN
+ EXECUTE $$SELECT XMLCONCAT( XMLELEMENT( name link, XMLATTRIBUTES( 'http://www.w3.org/1999/xhtml' AS xmlns, 'unapi-server' AS rel, $1 AS href, 'unapi' AS title)), $2)$$ INTO tmp_xml USING unapi_url, tmp_xml::XML;
+ END IF;
+
+ IF header_xml IS NOT NULL THEN tmp_xml := XMLCONCAT(header_xml,tmp_xml::XML); END IF;
+
+ element_list := regexp_split_to_array(layout.feed_top,E'\\.');
+ FOR i IN REVERSE ARRAY_UPPER(element_list, 1) .. 1 LOOP
+ EXECUTE 'SELECT XMLELEMENT( name '|| quote_ident(element_list[i]) ||', XMLATTRIBUTES( $1 AS xmlns), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML;
+ END LOOP;
+
+ RETURN tmp_xml::XML;
+END;
+$F$ LANGUAGE PLPGSQL STABLE;
+
+
+DROP FUNCTION IF EXISTS unapi.mmr(
+ obj_id BIGINT,
+ format TEXT,
+ ename TEXT,
+ includes TEXT[],
+ org TEXT,
+ depth INT,
+ slimit HSTORE,
+ soffset HSTORE,
+ include_xmlns BOOL,
+ pref_lib INT
+);
+
+CREATE OR REPLACE FUNCTION unapi.mmr (
+ obj_id BIGINT,
+ format TEXT,
+ ename TEXT,
+ includes TEXT[],
+ org TEXT,
+ depth INT DEFAULT NULL,
+ slimit HSTORE DEFAULT NULL,
+ soffset HSTORE DEFAULT NULL,
+ include_xmlns BOOL DEFAULT TRUE,
+ pref_lib INT DEFAULT NULL,
+ library_group INT DEFAULT NULL
+)
+RETURNS XML AS $F$
+DECLARE
+ mmrec metabib.metarecord%ROWTYPE;
+ leadrec biblio.record_entry%ROWTYPE;
+ subrec biblio.record_entry%ROWTYPE;
+ layout unapi.bre_output_layout%ROWTYPE;
+ xfrm config.xml_transform%ROWTYPE;
+ ouid INT;
+ xml_buf TEXT; -- growing XML document
+ tmp_xml TEXT; -- single-use XML string
+ xml_frag TEXT; -- single-use XML fragment
+ top_el TEXT;
+ output XML;
+ hxml XML;
+ axml XML;
+ subxml XML; -- subordinate records elements
+ sub_xpath TEXT;
+ parts TEXT[];
+BEGIN
+
+ -- xpath for extracting bre.marc values from subordinate records
+ -- so they may be appended to the MARC of the master record prior
+ -- to XSLT processing.
+ -- subjects, isbn, issn, upc -- anything else?
+ sub_xpath :=
+ '//*[starts-with(@tag, "6") or @tag="020" or @tag="022" or @tag="024"]';
+
+ IF org = '-' OR org IS NULL THEN
+ SELECT shortname INTO org FROM evergreen.org_top();
+ END IF;
+
+ SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
+
+ IF ouid IS NULL THEN
+ RETURN NULL::XML;
+ END IF;
+
+ SELECT INTO mmrec * FROM metabib.metarecord WHERE id = obj_id;
+ IF NOT FOUND THEN
+ RETURN NULL::XML;
+ END IF;
+
+ -- TODO: aggregate holdings from constituent records
+ IF format = 'holdings_xml' THEN -- the special case
+ output := unapi.mmr_holdings_xml(
+ obj_id, ouid, org, depth,
+ array_remove(includes,'holdings_xml'),
+ slimit, soffset, include_xmlns, pref_lib, library_group);
+ RETURN output;
+ END IF;
+
+ SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
+
+ IF layout.name IS NULL THEN
+ RETURN NULL::XML;
+ END IF;
+
+ SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform;
+
+ SELECT INTO leadrec * FROM biblio.record_entry WHERE id = mmrec.master_record;
+
+ -- Grab distinct MVF for all records if requested
+ IF ('mra' = ANY (includes)) THEN
+ axml := unapi.mmr_mra(obj_id,NULL,NULL,NULL,org,depth,NULL,NULL,TRUE,pref_lib);
+ ELSE
+ axml := NULL::XML;
+ END IF;
+
+ xml_buf = leadrec.marc;
+
+ hxml := NULL::XML;
+ IF ('holdings_xml' = ANY (includes)) THEN
+ hxml := unapi.mmr_holdings_xml(
+ obj_id, ouid, org, depth,
+ array_remove(includes,'holdings_xml'),
+ slimit, soffset, include_xmlns, pref_lib, library_group);
+ END IF;
+
+ subxml := NULL::XML;
+ parts := '{}'::TEXT[];
+ FOR subrec IN SELECT bre.* FROM biblio.record_entry bre
+ JOIN metabib.metarecord_source_map mmsm ON (mmsm.source = bre.id)
+ JOIN metabib.metarecord mmr ON (mmr.id = mmsm.metarecord)
+ WHERE mmr.id = obj_id AND NOT bre.deleted
+ ORDER BY CASE WHEN bre.id = mmr.master_record THEN 0 ELSE bre.id END
+ LIMIT COALESCE((slimit->'bre')::INT, 5) LOOP
+
+ IF subrec.id = leadrec.id THEN CONTINUE; END IF;
+ -- Append choice data from the the non-lead records to the
+ -- the lead record document
+
+ parts := parts || xpath(sub_xpath, subrec.marc::XML)::TEXT[];
+ END LOOP;
+
+ SELECT STRING_AGG( DISTINCT p , '' )::XML INTO subxml FROM UNNEST(parts) p;
+
+ -- append data from the subordinate records to the
+ -- main record document before applying the XSLT
+
+ IF subxml IS NOT NULL THEN
+ xml_buf := REGEXP_REPLACE(xml_buf,
+ '</record>(.*?)$', subxml || '</record>' || E'\\1');
+ END IF;
+
+ IF format = 'marcxml' THEN
+ -- If we're not using the prefixed namespace in
+ -- this record, then remove all declarations of it
+ IF xml_buf !~ E'<marc:' THEN
+ xml_buf := REGEXP_REPLACE(xml_buf,
+ ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g');
+ END IF;
+ ELSE
+ xml_buf := oils_xslt_process(xml_buf, xfrm.xslt)::XML;
+ END IF;
+
+ -- update top_el to reflect the change in xml_buf, which may
+ -- now be a different type of document (e.g. record -> mods)
+ top_el := REGEXP_REPLACE(xml_buf, E'^.*?<((?:\\S+:)?' ||
+ layout.holdings_element || ').*$', E'\\1');
+
+ IF axml IS NOT NULL THEN
+ xml_buf := REGEXP_REPLACE(xml_buf,
+ '</' || top_el || '>(.*?)$', axml || '</' || top_el || E'>\\1');
+ END IF;
+
+ IF hxml IS NOT NULL THEN
+ xml_buf := REGEXP_REPLACE(xml_buf,
+ '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1');
+ END IF;
+
+ IF ('mmr.unapi' = ANY (includes)) THEN
+ output := REGEXP_REPLACE(
+ xml_buf,
+ '</' || top_el || '>(.*?)',
+ XMLELEMENT(
+ name abbr,
+ XMLATTRIBUTES(
+ 'http://www.w3.org/1999/xhtml' AS xmlns,
+ 'unapi-id' AS class,
+ 'tag:open-ils.org:U2@mmr/' || obj_id || '/' || org AS title
+ )
+ )::TEXT || '</' || top_el || E'>\\1'
+ );
+ ELSE
+ output := xml_buf;
+ END IF;
+
+ -- remove ignorable whitesace
+ output := REGEXP_REPLACE(output::TEXT,E'>\\s+<','><','gs')::XML;
+ RETURN output;
+END;
+$F$ LANGUAGE PLPGSQL STABLE;
+
+DROP FUNCTION IF EXISTS unapi.mmr_holdings_xml (
+ mid BIGINT,
+ ouid INT,
+ org TEXT,
+ depth INT,
+ includes TEXT[],
+ slimit HSTORE,
+ soffset HSTORE,
+ include_xmlns BOOL,
+ pref_lib INT
+);
+
+CREATE OR REPLACE FUNCTION unapi.mmr_holdings_xml (
+ mid BIGINT,
+ ouid INT,
+ org TEXT,
+ depth INT DEFAULT NULL,
+ includes TEXT[] DEFAULT NULL::TEXT[],
+ slimit HSTORE DEFAULT NULL,
+ soffset HSTORE DEFAULT NULL,
+ include_xmlns BOOL DEFAULT TRUE,
+ pref_lib INT DEFAULT NULL,
+ library_group INT DEFAULT NULL
+)
+RETURNS XML AS $F$
+ SELECT XMLELEMENT(
+ name holdings,
+ XMLATTRIBUTES(
+ CASE WHEN $8 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+ CASE WHEN ('mmr' = ANY ($5)) THEN 'tag:open-ils.org:U2@mmr/' || $1 || '/' || $3 ELSE NULL END AS id,
+ (SELECT metarecord_has_holdable_copy FROM asset.metarecord_has_holdable_copy($1)) AS has_holdable
+ ),
+ XMLELEMENT(
+ name counts,
+ (SELECT XMLAGG(XMLELEMENT::XML) FROM (
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('public' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.opac_ou_metarecord_copy_count($2, $1)
+ UNION
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('staff' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.staff_ou_metarecord_copy_count($2, $1)
+ UNION
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('pref_lib' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.opac_ou_metarecord_copy_count($9, $1)
+ UNION
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('public' as type, depth, library_group, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.opac_lasso_metarecord_copy_count_sum(library_group, $1)
+ UNION
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('staff' as type, depth, library_group, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.staff_lasso_metarecord_copy_count_sum(library_group, $1)
+ ORDER BY 1
+ )x)
+ ),
+ -- XXX monograph_parts and foreign_copies are skipped in MRs ... put them back some day?
+ XMLELEMENT(
+ name volumes,
+ (SELECT XMLAGG(acn ORDER BY rank, name, label_sortkey) FROM (
+ -- Physical copies
+ SELECT unapi.acn(y.id,'xml','volume',array_remove( array_remove($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE), y.rank, name, label_sortkey
+ FROM evergreen.ranked_volumes((SELECT ARRAY_AGG(source) FROM metabib.metarecord_source_map WHERE metarecord = $1), $2, $4, $6, $7, $9, $5) AS y
+ UNION ALL
+ -- Located URIs
+ SELECT unapi.acn(uris.id,'xml','volume',array_remove( array_remove($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE), uris.rank, name, label_sortkey
+ FROM evergreen.located_uris((SELECT ARRAY_AGG(source) FROM metabib.metarecord_source_map WHERE metarecord = $1), $2, $9) AS uris
+ )x)
+ ),
+ CASE WHEN ('ssub' = ANY ($5)) THEN
+ XMLELEMENT(
+ name subscriptions,
+ (SELECT XMLAGG(ssub) FROM (
+ SELECT unapi.ssub(id,'xml','subscription','{}'::TEXT[], $3, $4, $6, $7, FALSE)
+ FROM serial.subscription
+ WHERE record_entry IN (SELECT source FROM metabib.metarecord_source_map WHERE metarecord = $1)
+ )x)
+ )
+ ELSE NULL END
+ );
+$F$ LANGUAGE SQL STABLE;
+
+
+SELECT evergreen.upgrade_deps_block_check('1465', :eg_version);
+
+ALTER TABLE config.ui_staff_portal_page_entry
+ADD COLUMN url_newtab boolean;
+
+
+SELECT evergreen.upgrade_deps_block_check('1466', :eg_version);
+
+INSERT into config.workstation_setting_type
+ (name, grp, label, description, datatype)
+VALUES (
+ 'ui.staff.disable_links_newtabs',
+ 'gui',
+ oils_i18n_gettext('ui.staff.disable_links_newtabs',
+ 'Staff Client: no new tabs',
+ 'coust', 'label'),
+ oils_i18n_gettext('ui.staff.disable_links_newtabs',
+ 'Prevents links in the staff interface from opening in new tabs or windows.',
+ 'coust', 'description'),
+ 'bool'
+);
+
+
+SELECT evergreen.upgrade_deps_block_check('1467', :eg_version);
+
+CREATE TABLE action.eresource_link_click (
+ id BIGSERIAL PRIMARY KEY,
+ clicked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ url TEXT,
+ record BIGINT NOT NULL REFERENCES biblio.record_entry (id)
+);
+
+CREATE TABLE action.eresource_link_click_course (
+ id SERIAL PRIMARY KEY,
+ click BIGINT NOT NULL REFERENCES action.eresource_link_click (id) ON DELETE CASCADE,
+ course INT REFERENCES asset.course_module_course (id) ON UPDATE CASCADE ON DELETE SET NULL,
+ course_name TEXT NOT NULL,
+ course_number TEXT NOT NULL
+);
+
+INSERT INTO config.global_flag (name, label, enabled)
+ VALUES (
+ 'opac.eresources.link_click_tracking',
+ oils_i18n_gettext('opac.eresources.link_click_tracking',
+ 'Track clicks on eresources links. Before enabling this global flag, be sure that you are monitoring disk space on your database server and have a cron job set up to delete click records after the desired retention interval.',
+ 'cgf', 'label'),
+ FALSE
+ );
+
+CREATE FUNCTION action.delete_old_eresource_link_clicks(days integer)
+ RETURNS VOID AS
+ 'DELETE FROM action.eresource_link_click
+ WHERE clicked_at < current_timestamp
+ - ($1::text || '' days'')::interval'
+ LANGUAGE SQL
+ VOLATILE;
+
+
+
+SELECT evergreen.upgrade_deps_block_check('1468', :eg_version);
+
+-- Necessary pre-seed data
+
+
+INSERT INTO actor.passwd_type (code, name, login, crypt_algo, iter_count)
+ VALUES ('api', 'OpenAPI Integration Password', TRUE, 'bf', 10)
+ON CONFLICT DO NOTHING;
+
+-- Move top-level perms "down" ...
+INSERT INTO permission.grp_perm_map (grp,perm,depth,grantable)
+ SELECT DISTINCT g.id, p.perm, p.depth, p.grantable
+ FROM permission.grp_perm_map p,
+ permission.grp_tree g
+ WHERE g.parent = 1 AND p.grp = 1;
+
+-- ... then remove the User version ...
+DELETE FROM permission.grp_perm_map WHERE grp = 1;
+
+-- ... and add a new branch to the group tree for API perms
+INSERT INTO permission.grp_tree (name, parent, description, application_perm)
+ VALUES ('API Integrator', 1, 'API Integration Accounts', 'group_application.api_integrator');
+
+INSERT INTO permission.grp_tree (name, parent, description, application_perm) SELECT 'Patron API', id, 'Patron API', 'group_application.api_integrator' FROM permission.grp_tree WHERE name = 'API Integrator';
+INSERT INTO permission.grp_tree (name, parent, description, application_perm) SELECT 'Org Unit API', id, 'Org Unit API', 'group_application.api_integrator' FROM permission.grp_tree WHERE name = 'API Integrator';
+INSERT INTO permission.grp_tree (name, parent, description, application_perm) SELECT 'Bib Record API', id, 'Bib Record API', 'group_application.api_integrator' FROM permission.grp_tree WHERE name = 'API Integrator';
+INSERT INTO permission.grp_tree (name, parent, description, application_perm) SELECT 'Item Record API', id, 'Item Record API', 'group_application.api_integrator' FROM permission.grp_tree WHERE name = 'API Integrator';
+INSERT INTO permission.grp_tree (name, parent, description, application_perm) SELECT 'Holds API', id, 'Holds API', 'group_application.api_integrator' FROM permission.grp_tree WHERE name = 'API Integrator';
+INSERT INTO permission.grp_tree (name, parent, description, application_perm) SELECT 'Debt Collection API', id, 'Debt Collection API', 'group_application.api_integrator' FROM permission.grp_tree WHERE name = 'API Integrator';
+INSERT INTO permission.grp_tree (name, parent, description, application_perm) SELECT 'Course Reserves API', id, 'Course Reserves API', 'group_application.api_integrator' FROM permission.grp_tree WHERE name = 'API Integrator';
+
+INSERT INTO permission.perm_list (code) VALUES
+ ('group_application.api_integrator'),
+ ('API_LOGIN'),
+ ('REST.api'),
+ ('REST.api.patrons'),
+ ('REST.api.orgs'),
+ ('REST.api.bibs'),
+ ('REST.api.items'),
+ ('REST.api.holds'),
+ ('REST.api.collections'),
+ ('REST.api.courses')
+ --- ... etc
+ON CONFLICT DO NOTHING;
+
+INSERT INTO permission.grp_perm_map (grp,perm,depth)
+ SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='API Integrator' AND p.code IN ('API_LOGIN','REST.api')
+ UNION
+ SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Patron API' AND p.code = 'REST.api.patrons'
+ UNION
+ SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Org Unit API' AND p.code = 'REST.api.orgs'
+ UNION
+ SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Bib Record API' AND p.code = 'REST.api.bibs'
+ UNION
+ SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Item Record API' AND p.code = 'REST.api.items'
+ UNION
+ SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Holds API' AND p.code = 'REST.api.holds'
+ UNION
+ SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Debt Collection API' AND p.code = 'REST.api.collections'
+ UNION
+ SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Course Reserves API' AND p.code = 'REST.api.courses'
+ON CONFLICT DO NOTHING;
+
+DROP SCHEMA IF EXISTS openapi CASCADE;
+CREATE SCHEMA IF NOT EXISTS openapi;
+
+CREATE TABLE IF NOT EXISTS openapi.integrator (
+ id INT PRIMARY KEY REFERENCES actor.usr (id),
+ enabled BOOL NOT NULL DEFAULT TRUE
+);
+
+CREATE TABLE IF NOT EXISTS openapi.json_schema_datatype (
+ name TEXT PRIMARY KEY,
+ label TEXT NOT NULL UNIQUE,
+ description TEXT
+);
+INSERT INTO openapi.json_schema_datatype (name,label) VALUES
+ ('boolean','Boolean'),
+ ('string','String'),
+ ('integer','Integer'),
+ ('number','Number'),
+ ('array','Array'),
+ ('object','Object')
+ON CONFLICT DO NOTHING;
+
+CREATE TABLE IF NOT EXISTS openapi.json_schema_format (
+ name TEXT PRIMARY KEY,
+ label TEXT NOT NULL UNIQUE,
+ description TEXT
+);
+INSERT INTO openapi.json_schema_format (name,label) VALUES
+ ('date-time','Timestamp'),
+ ('date','Date'),
+ ('time','Time'),
+ ('interval','Interval'),
+ ('email','Email Address'),
+ ('uri','URI'),
+ ('identifier','Opaque Identifier'),
+ ('money','Money'),
+ ('float','Floating Point Number'),
+ ('int64','Large Integer'),
+ ('password','Password')
+ON CONFLICT DO NOTHING;
+
+CREATE TABLE IF NOT EXISTS openapi.rate_limit_definition (
+ id SERIAL PRIMARY KEY,
+ name TEXT UNIQUE NOT NULL, -- i18n
+ limit_interval INTERVAL NOT NULL,
+ limit_count INT NOT NULL
+);
+SELECT SETVAL('openapi.rate_limit_definition_id_seq'::TEXT, 100);
+INSERT INTO openapi.rate_limit_definition (id, name, limit_interval, limit_count) VALUES
+ (1, 'Once per second', '1 second', 1),
+ (2, 'Ten per minute', '1 minute', 10),
+ (3, 'One hunderd per hour', '1 hour', 100),
+ (4, 'One thousand per hour', '1 hour', 1000),
+ (5, 'One thousand per 24 hour period', '24 hours', 1000),
+ (6, 'Ten thousand per 24 hour period', '24 hours', 10000),
+ (7, 'Unlimited', '1 second', 1000000),
+ (8, 'One hundred per second', '1 second', 100)
+ON CONFLICT DO NOTHING;
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint (
+ operation_id TEXT PRIMARY KEY,
+ path TEXT NOT NULL,
+ http_method TEXT NOT NULL CHECK (http_method IN ('get','put','post','delete','patch')),
+ security TEXT NOT NULL DEFAULT 'bearerAuth' CHECK (security IN ('bearerAuth','basicAuth','cookieAuth','paramAuth')),
+ summary TEXT NOT NULL,
+ method_source TEXT NOT NULL, -- perl module or opensrf application, tested by regex and assumes opensrf app name contains a "."
+ method_name TEXT NOT NULL,
+ method_params TEXT, -- eg, 'eg_auth_token hold' or 'eg_auth_token eg_user_id circ req.json'
+ active BOOL NOT NULL DEFAULT TRUE,
+ rate_limit INT REFERENCES openapi.rate_limit_definition (id),
+ CONSTRAINT path_and_method_once UNIQUE (path, http_method)
+);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_param (
+ id SERIAL PRIMARY KEY,
+ endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ name TEXT NOT NULL CHECK (name ~ '^\w+$'),
+ required BOOL NOT NULL DEFAULT FALSE,
+ in_part TEXT NOT NULL DEFAULT 'query' CHECK (in_part IN ('path','query','header','cookie')),
+ fm_type TEXT,
+ schema_type TEXT REFERENCES openapi.json_schema_datatype (name),
+ schema_format TEXT REFERENCES openapi.json_schema_format (name),
+ array_items TEXT REFERENCES openapi.json_schema_datatype (name),
+ default_value TEXT,
+ CONSTRAINT endpoint_and_name_once UNIQUE (endpoint, name),
+ CONSTRAINT format_requires_type CHECK (schema_format IS NULL OR schema_type IS NOT NULL),
+ CONSTRAINT array_items_requires_array_type CHECK (array_items IS NULL OR schema_type = 'array')
+);
+CREATE TABLE IF NOT EXISTS openapi.endpoint_response (
+ id SERIAL PRIMARY KEY,
+ endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ validate BOOL NOT NULL DEFAULT TRUE,
+ status INT NOT NULL DEFAULT 200,
+ content_type TEXT NOT NULL DEFAULT 'application/json',
+ description TEXT NOT NULL DEFAULT 'Success',
+ fm_type TEXT,
+ schema_type TEXT REFERENCES openapi.json_schema_datatype (name),
+ schema_format TEXT REFERENCES openapi.json_schema_format (name),
+ array_items TEXT REFERENCES openapi.json_schema_datatype (name),
+ CONSTRAINT endpoint_status_content_type_once UNIQUE (endpoint, status, content_type)
+);
+
+CREATE TABLE IF NOT EXISTS openapi.perm_set (
+ id SERIAL PRIMARY KEY,
+ name TEXT NOT NULL
+); -- push sequence value
+SELECT SETVAL('openapi.perm_set_id_seq'::TEXT, 1001);
+INSERT INTO openapi.perm_set (id, name) VALUES
+ (1,'Self - API only'),
+ (2,'Patrons - API only'),
+ (3,'Orgs - API only'),
+ (4,'Bibs - API only'),
+ (5,'Items - API only'),
+ (6,'Holds - API only'),
+ (7,'Collections - API only'),
+ (8,'Courses - API only'),
+
+ (101,'Self - standard permissions'),
+ (102,'Patrons - standard permissions'),
+ (103,'Orgs - standard permissions'),
+ (104,'Bibs - standard permissions'),
+ (105,'Items - standard permissions'),
+ (106,'Holds - standard permissions'),
+ (107,'Collections - standard permissions'),
+ (108,'Courses - standard permissions')
+ON CONFLICT DO NOTHING;
+
+CREATE TABLE IF NOT EXISTS openapi.perm_set_perm_map (
+ id SERIAL PRIMARY KEY,
+ perm_set INT NOT NULL REFERENCES openapi.perm_set (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ perm INT NOT NULL REFERENCES permission.perm_list (id) ON UPDATE CASCADE ON DELETE CASCADE
+);
+INSERT INTO openapi.perm_set_perm_map (perm_set, perm)
+ SELECT 1, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api')
+ UNION
+ SELECT 2, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.patrons')
+ UNION
+ SELECT 3, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.orgs')
+ UNION
+ SELECT 4, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.bibs')
+ UNION
+ SELECT 5, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.items')
+ UNION
+ SELECT 6, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.holds')
+ UNION
+ SELECT 7, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.collections')
+ UNION
+ SELECT 8, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.cources')
+ UNION
+ -- ...
+ SELECT 101, id FROM permission.perm_list WHERE code IN ('OPAC_LOGIN')
+ UNION
+ SELECT 102, id FROM permission.perm_list WHERE code IN ('STAFF_LOGIN','VIEW_USER')
+ UNION
+ SELECT 103, id FROM permission.perm_list WHERE code IN ('OPAC_LOGIN')
+ UNION
+ SELECT 104, id FROM permission.perm_list WHERE code IN ('OPAC_LOGIN')
+ UNION
+ SELECT 105, id FROM permission.perm_list WHERE code IN ('OPAC_LOGIN')
+ UNION
+ SELECT 106, id FROM permission.perm_list WHERE code IN ('STAFF_LOGIN','VIEW_USER')
+ UNION
+ SELECT 107, id FROM permission.perm_list WHERE code IN ('STAFF_LOGIN','VIEW_USER')
+ UNION
+ SELECT 108, id FROM permission.perm_list WHERE code IN ('STAFF_LOGIN')
+
+ON CONFLICT DO NOTHING;
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_perm_set_map (
+ id SERIAL PRIMARY KEY,
+ endpoint TEXT NOT NULL REFERENCES openapi.endpoint(operation_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ perm_set INT NOT NULL REFERENCES openapi.perm_set (id) ON UPDATE CASCADE ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_set (
+ name TEXT PRIMARY KEY,
+ description TEXT NOT NULL,
+ active BOOL NOT NULL DEFAULT TRUE,
+ rate_limit INT REFERENCES openapi.rate_limit_definition (id)
+);
+INSERT INTO openapi.endpoint_set (name, description) VALUES
+ ('self', 'Methods for retrieving and manipulating your own user account information'),
+ ('orgs', 'Methods for retrieving and manipulating organizational unit information'),
+ ('patrons', 'Methods for retrieving and manipulating patron information'),
+ ('holds', 'Methods for accessing and manipulating hold data'),
+ ('collections', 'Methods for accessing and manipulating patron debt collections data'),
+ ('bibs', 'Methods for accessing and manipulating bibliographic records and related data'),
+ ('items', 'Methods for accessing and manipulating barcoded item records'),
+ ('courses', 'Methods for accessing and manipulating course reserve data')
+ON CONFLICT DO NOTHING;
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_user_rate_limit_map (
+ id SERIAL PRIMARY KEY,
+ accessor INT NOT NULL REFERENCES actor.usr (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ rate_limit INT NOT NULL REFERENCES openapi.rate_limit_definition (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT endpoint_accessor_once UNIQUE (accessor, endpoint)
+);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_set_user_rate_limit_map (
+ id SERIAL PRIMARY KEY,
+ accessor INT NOT NULL REFERENCES actor.usr (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ endpoint_set TEXT NOT NULL REFERENCES openapi.endpoint_set (name) ON UPDATE CASCADE ON DELETE CASCADE,
+ rate_limit INT NOT NULL REFERENCES openapi.rate_limit_definition (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT endpoint_set_accessor_once UNIQUE (accessor, endpoint_set)
+);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_ip_rate_limit_map (
+ id SERIAL PRIMARY KEY,
+ ip_range INET NOT NULL,
+ endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ rate_limit INT NOT NULL REFERENCES openapi.rate_limit_definition (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT endpoint_ip_range_once UNIQUE (ip_range, endpoint)
+);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_set_ip_rate_limit_map (
+ id SERIAL PRIMARY KEY,
+ ip_range INET NOT NULL,
+ endpoint_set TEXT NOT NULL REFERENCES openapi.endpoint_set (name) ON UPDATE CASCADE ON DELETE CASCADE,
+ rate_limit INT NOT NULL REFERENCES openapi.rate_limit_definition (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT endpoint_set_ip_range_once UNIQUE (ip_range, endpoint_set)
+);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_set_endpoint_map (
+ id SERIAL PRIMARY KEY,
+ endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ endpoint_set TEXT NOT NULL REFERENCES openapi.endpoint_set (name) ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT endpoint_set_endpoint_once UNIQUE (endpoint_set, endpoint)
+);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_perm_map (
+ id SERIAL PRIMARY KEY,
+ endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ perm INT NOT NULL REFERENCES permission.perm_list (id) ON UPDATE CASCADE ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_set_perm_set_map (
+ id SERIAL PRIMARY KEY,
+ endpoint_set TEXT NOT NULL REFERENCES openapi.endpoint_set (name) ON UPDATE CASCADE ON DELETE CASCADE,
+ perm_set INT NOT NULL REFERENCES openapi.perm_set (id) ON UPDATE CASCADE ON DELETE CASCADE
+);
+INSERT INTO openapi.endpoint_set_perm_set_map (endpoint_set, perm_set) VALUES
+ ('self', 1), ('self', 101),
+ ('patrons', 2), ('patrons', 102),
+ ('orgs', 3), ('orgs', 103),
+ ('bibs', 4), ('bibs', 104),
+ ('items', 5), ('items', 105),
+ ('holds', 6), ('holds', 106),
+ ('collections', 7), ('collections', 107),
+ ('courses', 8), ('courses', 108)
+ON CONFLICT DO NOTHING;
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_set_perm_map (
+ id SERIAL PRIMARY KEY,
+ endpoint_set TEXT NOT NULL REFERENCES openapi.endpoint_set (name) ON UPDATE CASCADE ON DELETE CASCADE,
+ perm INT NOT NULL REFERENCES permission.perm_list (id) ON UPDATE CASCADE ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS openapi.authen_attempt_log (
+ request_id TEXT PRIMARY KEY,
+ attempt_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ ip_addr INET,
+ cred_user TEXT,
+ token TEXT
+);
+CREATE INDEX authen_cred_user_attempt_time_idx ON openapi.authen_attempt_log (attempt_time, cred_user);
+CREATE INDEX authen_ip_addr_attempt_time_idx ON openapi.authen_attempt_log (attempt_time, ip_addr);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_access_attempt_log (
+ request_id TEXT PRIMARY KEY,
+ attempt_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ allowed BOOL NOT NULL,
+ ip_addr INET,
+ accessor INT REFERENCES actor.usr (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ token TEXT
+);
+CREATE INDEX access_accessor_attempt_time_idx ON openapi.endpoint_access_attempt_log (accessor, attempt_time);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_dispatch_log (
+ request_id TEXT PRIMARY KEY,
+ complete_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ error BOOL NOT NULL
+);
+
+CREATE OR REPLACE FUNCTION actor.verify_passwd(pw_usr integer, pw_type text, test_passwd text) RETURNS boolean AS $f$
+DECLARE
+ pw_salt TEXT;
+ api_enabled BOOL;
+BEGIN
+ /* Returns TRUE if the password provided matches the in-db password.
+ * If the password type is salted, we compare the output of CRYPT().
+ * NOTE: test_passwd is MD5(salt || MD5(password)) for legacy
+ * 'main' passwords.
+ *
+ * Password type 'api' requires that the user be enabled as an
+ * integrator in the openapi.integrator table.
+ */
+
+ IF pw_type = 'api' THEN
+ SELECT enabled INTO api_enabled
+ FROM openapi.integrator
+ WHERE id = pw_usr;
+
+ IF NOT FOUND OR api_enabled IS FALSE THEN
+ -- API integrator account not registered
+ RETURN FALSE;
+ END IF;
+ END IF;
+
+ SELECT INTO pw_salt salt FROM actor.passwd
+ WHERE usr = pw_usr AND passwd_type = pw_type;
+
+ IF NOT FOUND THEN
+ -- no such password
+ RETURN FALSE;
+ END IF;
+
+ IF pw_salt IS NULL THEN
+ -- Password is unsalted, compare the un-CRYPT'ed values.
+ RETURN EXISTS (
+ SELECT TRUE FROM actor.passwd WHERE
+ usr = pw_usr AND
+ passwd_type = pw_type AND
+ passwd = test_passwd
+ );
+ END IF;
+
+ RETURN EXISTS (
+ SELECT TRUE FROM actor.passwd WHERE
+ usr = pw_usr AND
+ passwd_type = pw_type AND
+ passwd = CRYPT(test_passwd, pw_salt)
+ );
+END;
+$f$ STRICT LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION openapi.find_default_endpoint_rate_limit (target_endpoint TEXT) RETURNS openapi.rate_limit_definition AS $f$
+DECLARE
+ def_rl openapi.rate_limit_definition%ROWTYPE;
+BEGIN
+ -- Default rate limits can be applied at the endpoint or endpoint_set level;
+ -- endpoint overrides endpoint_set, and we choose the most restrictive from
+ -- the set if we have to look there.
+ SELECT d.* INTO def_rl
+ FROM openapi.rate_limit_definition d
+ JOIN openapi.endpoint e ON (e.rate_limit = d.id)
+ WHERE e.operation_id = target_endpoint;
+
+ IF NOT FOUND THEN
+ SELECT d.* INTO def_rl
+ FROM openapi.rate_limit_definition d
+ JOIN openapi.endpoint_set es ON (es.rate_limit = d.id)
+ JOIN openapi.endpoint_set_endpoint_map m ON (es.name = m.endpoint_set AND m.endpoint = target_endpoint)
+ -- This ORDER BY calculates the avg time between requests the user would have to wait to perfectly
+ -- avoid rate limiting. So, a bigger wait means it's more restrictive. We take the most restrictive
+ -- set-applied one.
+ ORDER BY EXTRACT(EPOCH FROM d.limit_interval) / d.limit_count::NUMERIC DESC
+ LIMIT 1;
+ END IF;
+
+ -- If there's no default for the endpoint or set, we provide 1/sec.
+ IF NOT FOUND THEN
+ def_rl.limit_interval := '1 second'::INTERVAL;
+ def_rl.limit_count := 1;
+ END IF;
+
+ RETURN def_rl;
+END;
+$f$ STABLE LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION openapi.find_user_endpoint_rate_limit (target_endpoint TEXT, accessing_usr INT) RETURNS openapi.rate_limit_definition AS $f$
+DECLARE
+ def_u_rl openapi.rate_limit_definition%ROWTYPE;
+BEGIN
+ SELECT d.* INTO def_u_rl
+ FROM openapi.rate_limit_definition d
+ JOIN openapi.endpoint_user_rate_limit_map e ON (e.rate_limit = d.id)
+ WHERE e.endpoint = target_endpoint
+ AND e.accessor = accessing_usr;
+
+ IF NOT FOUND THEN
+ SELECT d.* INTO def_u_rl
+ FROM openapi.rate_limit_definition d
+ JOIN openapi.endpoint_set_user_rate_limit_map e ON (e.rate_limit = d.id AND e.accessor = accessing_usr)
+ JOIN openapi.endpoint_set_endpoint_map m ON (e.endpoint_set = m.endpoint_set AND m.endpoint = target_endpoint)
+ ORDER BY EXTRACT(EPOCH FROM d.limit_interval) / d.limit_count::NUMERIC DESC
+ LIMIT 1;
+ END IF;
+
+ RETURN def_u_rl;
+END;
+$f$ STABLE LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION openapi.find_ip_addr_endpoint_rate_limit (target_endpoint TEXT, from_ip_addr INET) RETURNS openapi.rate_limit_definition AS $f$
+DECLARE
+ def_i_rl openapi.rate_limit_definition%ROWTYPE;
+BEGIN
+ SELECT d.* INTO def_i_rl
+ FROM openapi.rate_limit_definition d
+ JOIN openapi.endpoint_ip_rate_limit_map e ON (e.rate_limit = d.id)
+ WHERE e.endpoint = target_endpoint
+ AND e.ip_range && from_ip_addr
+ -- For IPs, we order first by the size of the ranges that we
+ -- matched (mask length), most specific (smallest block of IPs)
+ -- first, then by the restrictiveness of the limit, more restrictive first.
+ ORDER BY MASKLEN(e.ip_range) DESC, EXTRACT(EPOCH FROM d.limit_interval) / d.limit_count::NUMERIC DESC
+ LIMIT 1;
+
+ IF NOT FOUND THEN
+ SELECT d.* INTO def_i_rl
+ FROM openapi.rate_limit_definition d
+ JOIN openapi.endpoint_set_ip_rate_limit_map e ON (e.rate_limit = d.id AND ip_range && from_ip_addr)
+ JOIN openapi.endpoint_set_endpoint_map m ON (e.endpoint_set = m.endpoint_set AND m.endpoint = target_endpoint)
+ ORDER BY MASKLEN(e.ip_range) DESC, EXTRACT(EPOCH FROM d.limit_interval) / d.limit_count::NUMERIC DESC
+ LIMIT 1;
+ END IF;
+
+ RETURN def_i_rl;
+END;
+$f$ STABLE LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION openapi.check_generic_endpoint_rate_limit (target_endpoint TEXT, accessing_usr INT DEFAULT NULL, from_ip_addr INET DEFAULT NULL) RETURNS INT AS $f$
+DECLARE
+ def_rl openapi.rate_limit_definition%ROWTYPE;
+ def_u_rl openapi.rate_limit_definition%ROWTYPE;
+ def_i_rl openapi.rate_limit_definition%ROWTYPE;
+ u_wait INT;
+ i_wait INT;
+BEGIN
+ def_rl := openapi.find_default_endpoint_rate_limit(target_endpoint);
+
+ IF accessing_usr IS NOT NULL THEN
+ def_u_rl := openapi.find_user_endpoint_rate_limit(target_endpoint, accessing_usr);
+ END IF;
+
+ IF from_ip_addr IS NOT NULL THEN
+ def_i_rl := openapi.find_ip_addr_endpoint_rate_limit(target_endpoint, from_ip_addr);
+ END IF;
+
+ -- Now we test the user-based and IP-based limits in their focused way...
+ IF def_u_rl.id IS NOT NULL THEN
+ SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_u_rl.limit_interval) - NOW())) INTO u_wait
+ FROM (SELECT l.attempt_time,
+ COUNT(*) OVER (PARTITION BY l.accessor ORDER BY l.attempt_time DESC) AS running_count
+ FROM openapi.endpoint_access_attempt_log l
+ WHERE l.endpoint = target_endpoint
+ AND l.accessor = accessing_usr
+ AND l.attempt_time > NOW() - def_u_rl.limit_interval
+ ) x
+ WHERE running_count = def_u_rl.limit_count;
+ END IF;
+
+ IF def_i_rl.id IS NOT NULL THEN
+ SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_u_rl.limit_interval) - NOW())) INTO i_wait
+ FROM (SELECT l.attempt_time,
+ COUNT(*) OVER (PARTITION BY l.ip_addr ORDER BY l.attempt_time DESC) AS running_count
+ FROM openapi.endpoint_access_attempt_log l
+ WHERE l.endpoint = target_endpoint
+ AND l.ip_addr = from_ip_addr
+ AND l.attempt_time > NOW() - def_u_rl.limit_interval
+ ) x
+ WHERE running_count = def_u_rl.limit_count;
+ END IF;
+
+ -- If there are no user-specific or IP-based limit
+ -- overrides; check endpoint-wide limits for user,
+ -- then IP, and if we were passed neither, then limit
+ -- endpoint access for all users. Better to lock it
+ -- all down than to set the servers on fire.
+ IF COALESCE(u_wait, i_wait) IS NULL AND COALESCE(def_i_rl.id, def_u_rl.id) IS NULL THEN
+ IF accessing_usr IS NOT NULL THEN
+ SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_rl.limit_interval) - NOW())) INTO u_wait
+ FROM (SELECT l.attempt_time,
+ COUNT(*) OVER (PARTITION BY l.accessor ORDER BY l.attempt_time DESC) AS running_count
+ FROM openapi.endpoint_access_attempt_log l
+ WHERE l.endpoint = target_endpoint
+ AND l.accessor = accessing_usr
+ AND l.attempt_time > NOW() - def_rl.limit_interval
+ ) x
+ WHERE running_count = def_rl.limit_count;
+ ELSIF from_ip_addr IS NOT NULL THEN
+ SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_rl.limit_interval) - NOW())) INTO i_wait
+ FROM (SELECT l.attempt_time,
+ COUNT(*) OVER (PARTITION BY l.ip_addr ORDER BY l.attempt_time DESC) AS running_count
+ FROM openapi.endpoint_access_attempt_log l
+ WHERE l.endpoint = target_endpoint
+ AND l.ip_addr = from_ip_addr
+ AND l.attempt_time > NOW() - def_rl.limit_interval
+ ) x
+ WHERE running_count = def_rl.limit_count;
+ ELSE -- we have no user and no IP, global per-endpoint rate limit?
+ SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_rl.limit_interval) - NOW())) INTO i_wait
+ FROM (SELECT l.attempt_time,
+ COUNT(*) OVER (PARTITION BY l.endpoint ORDER BY l.attempt_time DESC) AS running_count
+ FROM openapi.endpoint_access_attempt_log l
+ WHERE l.endpoint = target_endpoint
+ AND l.attempt_time > NOW() - def_rl.limit_interval
+ ) x
+ WHERE running_count = def_rl.limit_count;
+ END IF;
+ END IF;
+
+ -- Send back the largest required wait time, or NULL for no restriction
+ u_wait := GREATEST(u_wait,i_wait);
+ IF u_wait > 0 THEN
+ RETURN u_wait;
+ END IF;
+
+ RETURN NULL;
+END;
+$f$ STABLE LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION openapi.check_auth_endpoint_rate_limit (accessing_usr TEXT DEFAULT NULL, from_ip_addr INET DEFAULT NULL) RETURNS INT AS $f$
+DECLARE
+ def_rl openapi.rate_limit_definition%ROWTYPE;
+ def_u_rl openapi.rate_limit_definition%ROWTYPE;
+ def_i_rl openapi.rate_limit_definition%ROWTYPE;
+ u_wait INT;
+ i_wait INT;
+BEGIN
+ def_rl := openapi.find_default_endpoint_rate_limit('authenticateUser');
+
+ IF accessing_usr IS NOT NULL THEN
+ SELECT (openapi.find_user_endpoint_rate_limit('authenticateUser', u.id)).* INTO def_u_rl
+ FROM actor.usr u
+ WHERE u.usrname = accessing_usr;
+ END IF;
+
+ IF from_ip_addr IS NOT NULL THEN
+ def_i_rl := openapi.find_ip_addr_endpoint_rate_limit('authenticateUser', from_ip_addr);
+ END IF;
+
+ -- Now we test the user-based and IP-based limits in their focused way...
+ IF def_u_rl.id IS NOT NULL THEN
+ SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_u_rl.limit_interval) - NOW())) INTO u_wait
+ FROM (SELECT l.attempt_time,
+ COUNT(*) OVER (PARTITION BY l.cred_user ORDER BY l.attempt_time DESC) AS running_count
+ FROM openapi.authen_attempt_log l
+ WHERE l.cred_user = accessing_usr
+ AND l.attempt_time > NOW() - def_u_rl.limit_interval
+ ) x
+ WHERE running_count = def_u_rl.limit_count;
+ END IF;
+
+ IF def_i_rl.id IS NOT NULL THEN
+ SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_u_rl.limit_interval) - NOW())) INTO i_wait
+ FROM (SELECT l.attempt_time,
+ COUNT(*) OVER (PARTITION BY l.ip_addr ORDER BY l.attempt_time DESC) AS running_count
+ FROM openapi.authen_attempt_log l
+ WHERE l.ip_addr = from_ip_addr
+ AND l.attempt_time > NOW() - def_u_rl.limit_interval
+ ) x
+ WHERE running_count = def_u_rl.limit_count;
+ END IF;
+
+ -- If there are no user-specific or IP-based limit
+ -- overrides; check endpoint-wide limits for user,
+ -- then IP, and if we were passed neither, then limit
+ -- endpoint access for all users. Better to lock it
+ -- all down than to set the servers on fire.
+ IF COALESCE(u_wait, i_wait) IS NULL AND COALESCE(def_i_rl.id, def_u_rl.id) IS NULL THEN
+ IF accessing_usr IS NOT NULL THEN
+ SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_rl.limit_interval) - NOW())) INTO u_wait
+ FROM (SELECT l.attempt_time,
+ COUNT(*) OVER (PARTITION BY l.cred_user ORDER BY l.attempt_time DESC) AS running_count
+ FROM openapi.authen_attempt_log l
+ WHERE l.cred_user = accessing_usr
+ AND l.attempt_time > NOW() - def_rl.limit_interval
+ ) x
+ WHERE running_count = def_rl.limit_count;
+ ELSIF from_ip_addr IS NOT NULL THEN
+ SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_rl.limit_interval) - NOW())) INTO i_wait
+ FROM (SELECT l.attempt_time,
+ COUNT(*) OVER (PARTITION BY l.ip_addr ORDER BY l.attempt_time DESC) AS running_count
+ FROM openapi.authen_attempt_log l
+ WHERE l.ip_addr = from_ip_addr
+ AND l.attempt_time > NOW() - def_rl.limit_interval
+ ) x
+ WHERE running_count = def_rl.limit_count;
+ ELSE -- we have no user and no IP, global auth attempt rate limit?
+ SELECT CEIL(EXTRACT(EPOCH FROM (l.attempt_time + def_rl.limit_interval) - NOW())) INTO u_wait
+ FROM openapi.authen_attempt_log l
+ WHERE l.attempt_time > NOW() - def_rl.limit_interval
+ ORDER BY l.attempt_time DESC
+ LIMIT 1 OFFSET def_rl.limit_count;
+ END IF;
+ END IF;
+
+ -- Send back the largest required wait time, or NULL for no restriction
+ u_wait := GREATEST(u_wait,i_wait);
+ IF u_wait > 0 THEN
+ RETURN u_wait;
+ END IF;
+
+ RETURN NULL;
+END;
+$f$ STABLE LANGUAGE PLPGSQL;
+
+--===================================== Seed data ==================================
+
+-- ===== authentication
+INSERT INTO openapi.endpoint (operation_id, path, security, http_method, summary, method_source, method_name, method_params, rate_limit) VALUES
+-- Builtin auth-related methods, give all users and IPs 100/s of each
+ ('authenticateUser', '/self/auth', 'basicAuth', 'get', 'Authenticate API user', 'OpenILS::OpenAPI::Controller', 'authenticateUser', 'param.u param.p param.t', 8),
+ ('logoutUser', '/self/auth', 'bearerAuth', 'delete', 'Logout API user', 'open-ils.auth', 'open-ils.auth.session.delete', 'eg_auth_token', 8)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value) VALUES
+ ('authenticateUser','u','query','string',NULL,NULL),
+ ('authenticateUser','p','query','string','password',NULL),
+ ('authenticateUser','t','query','string',NULL,'api')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('authenticateUser','object') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,content_type) VALUES ('authenticateUser','text/plain') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('logoutUser','object') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,content_type) VALUES ('logoutUser','text/plain') ON CONFLICT DO NOTHING;
+
+-- ===== self-service
+-- get/update me
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrieveSelfProfile', '/self/me', 'get', 'Return patron/user record for logged in user', 'OpenILS::OpenAPI::Controller::patron', 'deliver_user', 'eg_auth_token eg_user_id'),
+ ('selfUpdateParts', '/self/me', 'patch', 'Update portions of the logged in user''s record', 'OpenILS::OpenAPI::Controller::patron', 'update_user_parts', 'eg_auth_token req.json')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveSelfProfile','au') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('selfUpdateParts','object') ON CONFLICT DO NOTHING;
+
+-- get my standing penalties
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'selfActivePenalties',
+ '/self/standing_penalties',
+ 'get',
+ 'Produces patron-visible penalty details for the authorized account',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'standing_penalties',
+ 'eg_auth_token eg_user_id "1"'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('selfActivePenalties','array','object') ON CONFLICT DO NOTHING; -- array of fleshed ausp
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'selfPenalty',
+ '/self/standing_penalty/:penaltyid',
+ 'get',
+ 'Retrieve one penalty for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'standing_penalties',
+ 'eg_auth_token eg_user_id "1" param.penaltyid'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('selfPenalty','penaltyid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+
+
+
+-- manage my holds
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrieveSelfHolds', '/self/holds', 'get', 'Return unfilled holds for the authorized account', 'OpenILS::OpenAPI::Controller::hold', 'open_holds', 'eg_auth_token eg_user_id'),
+ ('requestSelfHold', '/self/holds', 'post', 'Request a hold for the authorized account', 'OpenILS::OpenAPI::Controller::hold', 'request_hold', 'eg_auth_token eg_user_id req.json'),
+ ('retrieveSelfHold', '/self/hold/:hold', 'get', 'Retrieve one hold for the logged in user', 'OpenILS::OpenAPI::Controller::hold', 'fetch_user_hold', 'eg_auth_token eg_user_id param.hold'),
+ ('updateSelfHold', '/self/hold/:hold', 'patch', 'Update one hold for the logged in user', 'OpenILS::OpenAPI::Controller::hold', 'update_user_hold', 'eg_auth_token eg_user_id param.hold req.json'),
+ ('cancelSelfHold', '/self/hold/:hold', 'delete', 'Cancel one hold for the logged in user', 'OpenILS::OpenAPI::Controller::hold', 'cancel_user_hold', 'eg_auth_token eg_user_id param.hold "6"')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES
+ ('retrieveSelfHold','hold','path','integer',TRUE),
+ ('updateSelfHold','hold','path','integer',TRUE),
+ ('cancelSelfHold','hold','path','integer',TRUE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveSelfHolds','array','object') ON CONFLICT DO NOTHING;
+
+
+-- general xact list
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrieveSelfXacts',
+ '/self/transactions/:state',
+ 'get',
+ 'Produces a list of transactions of the logged in user',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'transactions_by_state',
+ 'eg_auth_token eg_user_id state param.limit param.offset param.sort param.before param.after'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value,required) VALUES
+ ('retrieveSelfXacts','state','path','string',NULL,NULL,TRUE),
+ ('retrieveSelfXacts','limit','query','integer',NULL,NULL,FALSE),
+ ('retrieveSelfXacts','offset','query','integer',NULL,'0',FALSE),
+ ('retrieveSelfXacts','sort','query','string',NULL,'desc',FALSE),
+ ('retrieveSelfXacts','before','query','string','date-time',NULL,FALSE),
+ ('retrieveSelfXacts','after','query','string','date-time',NULL,FALSE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveSelfXacts','array','object') ON CONFLICT DO NOTHING;
+
+-- general xact detail
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrieveSelfXact',
+ '/self/transaction/:id',
+ 'get',
+ 'Details of one transaction for the logged in user',
+ 'open-ils.actor',
+ 'open-ils.actor.user.transaction.fleshed.retrieve',
+ 'eg_auth_token param.id'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveSelfXact','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrieveSelfCircs', '/self/checkouts', 'get', 'Open Circs for the logged in user', 'open-ils.circ', 'open-ils.circ.actor.user.checked_out.atomic', 'eg_auth_token eg_user_id'),
+ ('requestSelfCirc', '/self/checkouts', 'post', 'Attempt a circulation for the logged in user', 'OpenILS::OpenAPI::Controller::patron', 'checkout_item', 'eg_auth_token eg_user_id req.json')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveSelfCircs','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrieveSelfCircHistory',
+ '/self/checkouts/history',
+ 'get',
+ 'Historical Circs for logged in user',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'circulation_history',
+ 'eg_auth_token eg_user_id param.limit param.offset param.sort param.before param.after'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value) VALUES
+ ('retrieveSelfCircHistory','limit','query','integer',NULL,NULL),
+ ('retrieveSelfCircHistory','offset','query','integer',NULL,'0'),
+ ('retrieveSelfCircHistory','sort','query','string',NULL,'desc'),
+ ('retrieveSelfCircHistory','before','query','string','date-time',NULL),
+ ('retrieveSelfCircHistory','after','query','string','date-time',NULL)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveSelfCircHistory','array','object') ON CONFLICT DO NOTHING;
+
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrieveSelfCirc', '/self/checkout/:id', 'get', 'Retrieve one circulation for the logged in user', 'open-ils.actor', 'open-ils.actor.user.transaction.fleshed.retrieve', 'eg_auth_token param.id'),
+ ('renewSelfCirc', '/self/checkout/:id', 'put', 'Renew one circulation for the logged in user', 'OpenILS::OpenAPI::Controller::patron', 'renew_circ', 'eg_auth_token param.id eg_user_id'),
+ ('checkinSelfCirc', '/self/checkout/:id', 'delete', 'Check in one circulation for the logged in user', 'OpenILS::OpenAPI::Controller::patron', 'checkin_circ', 'eg_auth_token param.id eg_user_id')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES
+ ('retrieveSelfCirc','id','path','integer',TRUE),
+ ('renewSelfCirc','id','path','integer',TRUE),
+ ('checkinSelfCirc','id','path','integer',TRUE)
+ON CONFLICT DO NOTHING;
+
+-- bib, item, and org methods
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrieveOrgList',
+ '/org_units',
+ 'get',
+ 'List of org units',
+ 'OpenILS::OpenAPI::Controller::org',
+ 'flat_org_list',
+ 'every_param.field every_param.comparison every_param.value'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES
+ ('retrieveOrgList','field','query','string'),
+ ('retrieveOrgList','comparison','query','string'),
+ ('retrieveOrgList','value','query','string')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveOrgList','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrieveOneOrg',
+ '/org_unit/:id',
+ 'get',
+ 'One org unit',
+ 'OpenILS::OpenAPI::Controller::org',
+ 'one_org',
+ 'param.id'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveOneOrg','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveOneOrg','aou') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name) VALUES (
+ 'retrieveFullOrgTree',
+ '/org_tree',
+ 'get',
+ 'Full hierarchical tree of org units',
+ 'OpenILS::OpenAPI::Controller::org',
+ 'full_tree'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveFullOrgTree','aou') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrievePartialOrgTree',
+ '/org_tree/:id',
+ 'get',
+ 'Partial hierarchical tree of org units starting from a specific org unit',
+ 'OpenILS::OpenAPI::Controller::org',
+ 'one_tree',
+ 'param.id'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrievePartialOrgTree','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrievePartialOrgTree','aou') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'createOneBib',
+ '/bibs',
+ 'post',
+ 'Attempts to create a bibliographic record using MARCXML passed as the request content',
+ 'open-ils.cat',
+ 'open-ils.cat.biblio.record.xml.create',
+ 'eg_auth_token req.text param.sourcename'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES ('createOneBib','sourcename','query','string') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('createOneBib','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'updateOneBib',
+ '/bib/:id',
+ 'put',
+ 'Attempts to update a bibliographic record using MARCXML passed as the request content',
+ 'open-ils.cat',
+ 'open-ils.cat.biblio.record.marc.replace',
+ 'eg_auth_token param.id req.text param.sourcename'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('updateOneBib','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES ('updateOneBib','sourcename','query','string') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('updateOneBib','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'deleteOneBib',
+ '/bib/:id',
+ 'delete',
+ 'Attempts to delete a bibliographic record',
+ 'open-ils.cat',
+ 'open-ils.cat.biblio.record_entry.delete',
+ 'eg_auth_token param.id'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('deleteOneBib','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,validate) VALUES ('deleteOneBib','integer',FALSE) ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'updateBREParts',
+ '/bib/:id',
+ 'patch',
+ 'Attempts to update the biblio.record_entry metadata surrounding a bib record',
+ 'OpenILS::OpenAPI::Controller::bib',
+ 'update_bre_parts',
+ 'eg_auth_token param.id req.json'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('updateBREParts','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('updateBREParts','bre') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrieveOneBib',
+ '/bib/:id',
+ 'get',
+ 'Retrieve a bibliographic record, either full biblio::record_entry object, or just the MARCXML',
+ 'OpenILS::OpenAPI::Controller::bib',
+ 'fetch_one_bib',
+ 'param.id'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveOneBib','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveOneBib','bre') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,content_type) VALUES ('retrieveOneBib','application/xml') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,content_type) VALUES ('retrieveOneBib','application/octet-stream') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrieveOneBibHoldings',
+ '/bib/:id/holdings',
+ 'get',
+ 'Retrieve the holdings data for a bibliographic record',
+ 'OpenILS::OpenAPI::Controller::bib',
+ 'fetch_one_bib_holdings',
+ 'param.id'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,default_value,required) VALUES
+ ('retrieveOneBibHoldings','id','path','integer',NULL,TRUE),
+ ('retrieveOneBibHoldings','limit','query','integer',NULL,FALSE),
+ ('retrieveOneBibHoldings','offset','query','integer','0',FALSE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveOneBibHoldings','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'bibDisplayFields',
+ '/bib/:id/display_fields',
+ 'get',
+ 'Retrieve display-related data for a bibliographic record',
+ 'OpenILS::OpenAPI::Controller::bib',
+ 'fetch_one_bib_display_fields',
+ 'param.id req.text'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('bibDisplayFields','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('bibDisplayFields','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'newItems',
+ '/items/fresh',
+ 'get',
+ 'Retrieve a list of newly added items',
+ 'OpenILS::OpenAPI::Controller::bib',
+ 'fetch_new_items',
+ 'param.limit param.offset param.maxage'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,default_value) VALUES ('newItems','limit','query','integer','0') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,default_value) VALUES ('newItems','offset','query','integer','100') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format) VALUES ('newItems','maxage','query','string','interval') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('newItems','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrieveItem',
+ '/item/:barcode',
+ 'get',
+ 'Retrieve one item by its barcode',
+ 'OpenILS::OpenAPI::Controller::bib',
+ 'item_by_barcode',
+ 'param.barcode'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveItem','barcode','path','string',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,description,status,schema_type) VALUES ('retrieveItem','Item Lookup Failed','404','object') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveItem','acp') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'createItem',
+ '/items',
+ 'post',
+ 'Create an item record',
+ 'OpenILS::OpenAPI::Controller::bib',
+ 'create_or_update_one_item',
+ 'eg_auth_token req.json'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,description,status) VALUES ('createItem','Item Creation Failed','400') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('createItem','acp') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'updateItem',
+ '/item/:barcode',
+ 'patch',
+ 'Update a restricted set of item record fields',
+ 'OpenILS::OpenAPI::Controller::bib',
+ 'create_or_update_one_item',
+ 'eg_auth_token req.json param.barcode'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('updateItem','barcode','path','string',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,description,status) VALUES ('updateItem','Item Update Failed','400') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('updateItem','acp') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'deleteItem',
+ '/item/:barcode',
+ 'delete',
+ 'Delete one item record',
+ 'OpenILS::OpenAPI::Controller::bib',
+ 'delete_one_item',
+ 'eg_auth_token param.barcode'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('deleteItem','barcode','path','string',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,description,status,schema_type) VALUES ('deleteItem','Item Deletion Failed','404','object') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('deleteItem','boolean') ON CONFLICT DO NOTHING;
+
+
+-- === patron (non-self) methods
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'searchPatrons',
+ '/patrons',
+ 'get',
+ 'List of patrons matching requested conditions',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'find_users',
+ 'eg_auth_token every_param.field every_param.comparison every_param.value param.limit param.offset'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES
+ ('searchPatrons','field','query','string'),
+ ('searchPatrons','comparison','query','string'),
+ ('searchPatrons','value','query','string')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value) VALUES
+ ('searchPatrons','offset','query','integer',NULL,'0'),
+ ('searchPatrons','limit','query','integer',NULL,'100')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('searchPatrons','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'verifyUserCredentials',
+ '/patrons/verify',
+ 'get',
+ 'Verify the credentials for a user account',
+ 'open-ils.actor',
+ 'open-ils.actor.verify_user_password',
+ 'eg_auth_token param.barcode param.usrname "" param.password'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,required) VALUES
+ ('verifyUserCredentials','barcode','query','string',NULL,FALSE),
+ ('verifyUserCredentials','usrname','query','string',NULL,FALSE),
+ ('verifyUserCredentials','password','query','string','password',TRUE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('verifyUserCredentials','boolean') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrievePatronProfile', '/patron/:userid', 'get', 'Return patron/user record for the requested user', 'OpenILS::OpenAPI::Controller::patron', 'deliver_user', 'eg_auth_token param.userid')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrievePatronProfile','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrievePatronProfile','au') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronIdByCardBarcode',
+ '/patrons/by_barcode/:barcode/id',
+ 'get',
+ 'Retrieve patron id by barcode',
+ 'open-ils.actor',
+ 'open-ils.actor.user.retrieve_id_by_barcode_or_username',
+ 'eg_auth_token param.barcode'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronIdByCardBarcode','barcode','path','string',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('patronIdByCardBarcode','integer') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronIdByUsername',
+ '/patrons/by_username/:username/id',
+ 'get',
+ 'Retrieve patron id by username',
+ 'open-ils.actor',
+ 'open-ils.actor.user.retrieve_id_by_barcode_or_username',
+ 'eg_auth_token "" param.username'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronIdByUsername','username','path','string',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('patronIdByUsername','integer') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronByCardBarcode',
+ '/patrons/by_barcode/:barcode',
+ 'get',
+ 'Retrieve patron by barcode',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'user_by_identifier_string',
+ 'eg_auth_token param.barcode'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronByCardBarcode','barcode','path','string',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('patronByCardBarcode','au') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronByUsername',
+ '/patrons/by_username/:username',
+ 'get',
+ 'Retrieve patron by username',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'user_by_identifier_string',
+ 'eg_auth_token "" param.username'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronByUsername','username','path','string',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('patronByUsername','au') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrievePatronCircHistory',
+ '/patron/:userid/checkouts/history',
+ 'get',
+ 'Historical Circs for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'circulation_history',
+ 'eg_auth_token param.userid param.limit param.offset param.sort param.before param.after'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value,required) VALUES
+ ('retrievePatronCircHistory','userid','path','integer',NULL,NULL,TRUE),
+ ('retrievePatronCircHistory','limit','query','integer',NULL,NULL,FALSE),
+ ('retrievePatronCircHistory','offset','query','integer',NULL,'0',FALSE),
+ ('retrievePatronCircHistory','sort','query','string',NULL,'desc',FALSE),
+ ('retrievePatronCircHistory','before','query','string','date-time',NULL,FALSE),
+ ('retrievePatronCircHistory','after','query','string','date-time',NULL,FALSE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrievePatronCircHistory','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrievePatronHolds', '/patron/:userid/holds', 'get', 'Retrieve unfilled holds for a patron', 'OpenILS::OpenAPI::Controller::hold', 'open_holds', 'eg_auth_token param.userid'),
+ ('requestPatronHold', '/patron/:userid/holds', 'post', 'Request a hold for a patron', 'OpenILS::OpenAPI::Controller::hold', 'request_hold', 'eg_auth_token param.userid req.json'),
+ ('retrievePatronHold','/patron/:userid/hold/:hold', 'get', 'Retrieve one hold for a patron', 'OpenILS::OpenAPI::Controller::hold', 'fetch_user_hold', 'eg_auth_token param.userid param.hold'),
+ ('updatePatronHold', '/patron/:userid/hold/:hold', 'patch', 'Update one hold for a patron', 'OpenILS::OpenAPI::Controller::hold', 'update_user_hold', 'eg_auth_token param.userid param.hold req.json'),
+ ('cancelPatronHold', '/patron/:userid/hold/:hold', 'delete', 'Cancel one hold for a patron', 'OpenILS::OpenAPI::Controller::hold', 'cancel_user_hold', 'eg_auth_token param.userid param.hold "6"')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES
+ ('retrievePatronHolds','userid','path','integer',TRUE),
+ ('retrievePatronHold','userid','path','integer',TRUE),
+ ('retrievePatronHold','hold','path','integer',TRUE),
+ ('requestPatronHold','userid','path','integer',TRUE),
+ ('updatePatronHold','userid','path','integer',TRUE),
+ ('updatePatronHold','hold','path','integer',TRUE),
+ ('cancelPatronHold','userid','path','integer',TRUE),
+ ('cancelPatronHold','hold','path','integer',TRUE)
+ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrieveHoldPickupLocations', '/holds/pickup_locations', 'get', 'Retrieve all valid hold/reserve pickup locations', 'OpenILS::OpenAPI::Controller::hold', 'valid_hold_pickup_locations', '')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveHoldPickupLocations','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrieveHold', '/hold/:hold', 'get', 'Retrieve one hold object', 'open-ils.circ', 'open-ils.circ.hold.details.retrieve', 'eg_auth_token param.hold')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveHold','hold','path','integer',TRUE) ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrievePatronXacts',
+ '/patron/:userid/transactions/:state',
+ 'get',
+ 'Produces a list of transactions of the specified user',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'transactions_by_state',
+ 'eg_auth_token param.userid state param.limit param.offset param.sort param.before param.after'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value,required) VALUES
+ ('retrievePatronXacts','userid','path','integer',NULL,NULL,TRUE),
+ ('retrievePatronXacts','state','path','string',NULL,NULL,TRUE),
+ ('retrievePatronXacts','limit','query','integer',NULL,NULL,FALSE),
+ ('retrievePatronXacts','offset','query','integer',NULL,'0',FALSE),
+ ('retrievePatronXacts','sort','query','string',NULL,'desc',FALSE),
+ ('retrievePatronXacts','before','query','string','date-time',NULL,FALSE),
+ ('retrievePatronXacts','after','query','string','date-time',NULL,FALSE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrievePatronXacts','array','object') ON CONFLICT DO NOTHING;
+
+-- general xact detail
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrievePatronXact',
+ '/patron/:userid/transaction/:id',
+ 'get',
+ 'Details of one transaction for the specified user',
+ 'open-ils.actor',
+ 'open-ils.actor.user.transaction.fleshed.retrieve',
+ 'eg_auth_token param.id'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrievePatronXact','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrievePatronXact','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrievePatronCircs', '/patron/:userid/checkouts', 'get', 'Open Circs for a patron', 'open-ils.circ', 'open-ils.circ.actor.user.checked_out.atomic', 'eg_auth_token param.userid'),
+ ('requestPatronCirc', '/patron/:userid/checkouts', 'post', 'Attempt a circulation for a patron', 'OpenILS::OpenAPI::Controller::patron', 'checkout_item', 'eg_auth_token param.userid req.json')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrievePatronCircs','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('requestPatronCirc','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrievePatronCircs','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrievePatronCirc', '/patron/:userid/checkout/:id', 'get', 'Retrieve one circulation for the specified user', 'open-ils.actor', 'open-ils.actor.user.transaction.fleshed.retrieve', 'eg_auth_token param.id'),
+ ('renewPatronCirc', '/patron/:userid/checkout/:id', 'put', 'Renew one circulation for the specified user', 'OpenILS::OpenAPI::Controller::patron', 'renew_circ', 'eg_auth_token param.id param.userid'),
+ ('checkinPatronCirc', '/patron/:userid/checkout/:id', 'delete', 'Check in one circulation for the specified user', 'OpenILS::OpenAPI::Controller::patron', 'checkin_circ', 'eg_auth_token param.id param.userid')
+ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES
+ ('retrievePatronCirc','userid','path','integer',TRUE),
+ ('retrievePatronCirc','id','path','integer',TRUE),
+ ('renewPatronCirc','userid','path','integer',TRUE),
+ ('renewPatronCirc','id','path','integer',TRUE),
+ ('checkinPatronCirc','userid','path','integer',TRUE),
+ ('checkinPatronCirc','id','path','integer',TRUE)
+ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronATEvents',
+ '/patron/:userid/triggered_events',
+ 'get',
+ 'Retrieve a list of A/T events for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'usr_at_events',
+ 'eg_auth_token param.userid param.limit param.offset param.before param.after every_param.hook'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value,required) VALUES
+ ('patronATEvents','userid','path','integer',NULL,NULL,TRUE),
+ ('patronATEvents','limit','query','integer',NULL,'100',FALSE),
+ ('patronATEvents','offset','query','integer',NULL,'0',FALSE),
+ ('patronATEvents','before','query','string','date-time',NULL,FALSE),
+ ('patronATEvents','after','query','string','date-time',NULL,FALSE),
+ ('patronATEvents','hook','query','string',NULL,NULL,FALSE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('patronATEvents','array','integer') ON CONFLICT DO NOTHING; -- array of ausp ids
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronATEvent',
+ '/patron/:userid/triggered_event/:eventid',
+ 'get',
+ 'Retrieve one penalty for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'usr_at_events',
+ 'eg_auth_token param.userid "1" "0" "" "" "" param.eventid'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronATEvent','eventid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronATEvent','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronActivePenalties',
+ '/patron/:userid/standing_penalties',
+ 'get',
+ 'Retrieve all penalty details for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'standing_penalties',
+ 'eg_auth_token param.userid'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronActivePenalties','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('patronActivePenalties','array','integer') ON CONFLICT DO NOTHING; -- array of ausp ids
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronPenalty',
+ '/patron/:userid/standing_penalty/:penaltyid',
+ 'get',
+ 'Retrieve one penalty for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'standing_penalties',
+ 'eg_auth_token param.userid "0" param.penaltyid'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronPenalty','penaltyid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronPenalty','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronActiveMessages',
+ '/patron/:userid/messages',
+ 'get',
+ 'Retrieve all active message ids for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'usr_messages',
+ 'eg_auth_token param.userid "0"'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronActiveMessages','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('patronActiveMessages','array','integer') ON CONFLICT DO NOTHING; -- array of aum ids
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronMessage',
+ '/patron/:userid/message/:msgid',
+ 'get',
+ 'Retrieve one message for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'usr_messages',
+ 'eg_auth_token param.userid "0" param.msgid'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessage','msgid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessage','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('patronMessage','aum') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronMessageUpdate',
+ '/patron/:userid/message/:msgid',
+ 'patch',
+ 'Update one message for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'update_usr_message',
+ 'eg_auth_token eg_user_id param.userid param.msgid req.json'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessageUpdate','msgid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessageUpdate','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('patronMessageUpdate','aum') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronMessageArchive',
+ '/patron/:userid/message/:msgid',
+ 'delete',
+ 'Archive one message for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'archive_usr_message',
+ 'eg_auth_token eg_user_id param.userid param.msgid'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessageArchive','msgid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessageArchive','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('patronMessageArchive','boolean') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronActivityLog',
+ '/patron/:userid/activity',
+ 'get',
+ 'Retrieve patron activity (authen, authz, etc)',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'usr_activity',
+ 'eg_auth_token param.userid param.maxage param.limit param.offset param.sort'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value,required) VALUES
+ ('patronActivityLog','userid','path','integer',NULL,NULL,TRUE),
+ ('patronActivityLog','limit','query','integer',NULL,'100',FALSE),
+ ('patronActivityLog','offset','query','integer',NULL,'0',FALSE),
+ ('patronActivityLog','sort','query','string',NULL,'desc',FALSE),
+ ('patronActivityLog','maxage','query','string','date-time',NULL,FALSE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('patronActivityLog','array','object') ON CONFLICT DO NOTHING;
+
+------- collections
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'collectionsPatronsOfInterest',
+ '/collections/:shortname/users_of_interest',
+ 'get',
+ 'List of patrons to consider for collections based on the search criteria provided.',
+ 'open-ils.collections',
+ 'open-ils.collections.users_of_interest.retrieve',
+ 'eg_auth_token param.fine_age param.fine_amount param.shortname'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,required) VALUES
+ ('collectionsPatronsOfInterest','shortname','path','string',NULL,TRUE),
+ ('collectionsPatronsOfInterest','fine_age','query','integer',NULL,TRUE),
+ ('collectionsPatronsOfInterest','fine_amount','query','string','money',TRUE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items,validate) VALUES ('collectionsPatronsOfInterest','array','object',FALSE) ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'collectionsPatronsOfInterestWarning',
+ '/collections/:shortname/users_of_interest/warning',
+ 'get',
+ 'List of patrons with the PATRON_EXCEEDS_COLLECTIONS_WARNING penalty to consider for collections based on the search criteria provided.',
+ 'open-ils.collections',
+ 'open-ils.collections.users_of_interest.warning_penalty.retrieve',
+ 'eg_auth_token param.shortname param.min_age param.max_age'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,required) VALUES
+ ('collectionsPatronsOfInterestWarning','shortname','path','string',NULL,TRUE),
+ ('collectionsPatronsOfInterestWarning','min_age','query','string','date-time',FALSE),
+ ('collectionsPatronsOfInterestWarning','max_age','query','string','date-time',FALSE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items,validate) VALUES ('collectionsPatronsOfInterestWarning','array','object',FALSE) ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'collectionsGetPatronDetail',
+ '/patron/:usrid/collections/:shortname',
+ 'get',
+ 'Get collections-related transaction details for a patron.',
+ 'open-ils.collections',
+ 'open-ils.collections.user_transaction_details.retrieve',
+ 'eg_auth_token param.start param.end param.shortname every_param.usrid'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,required) VALUES
+ ('collectionsGetPatronDetail','usrid','path','integer',NULL,TRUE),
+ ('collectionsGetPatronDetail','shortname','path','string',NULL,TRUE),
+ ('collectionsGetPatronDetail','start','query','string','date-time',TRUE),
+ ('collectionsGetPatronDetail','end','query','string','date-time',TRUE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items,validate) VALUES ('collectionsGetPatronDetail','array','object',FALSE) ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'collectionsPutPatronInCollections',
+ '/patron/:usrid/collections/:shortname',
+ 'post',
+ 'Put patron into collections.',
+ 'open-ils.collections',
+ 'open-ils.collections.put_into_collections',
+ 'eg_auth_token param.usrid param.shortname param.fee param.note'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,required) VALUES
+ ('collectionsPutPatronInCollections','usrid','path','integer',NULL,TRUE),
+ ('collectionsPutPatronInCollections','shortname','path','string',NULL,TRUE),
+ ('collectionsPutPatronInCollections','fee','query','string','money',FALSE),
+ ('collectionsPutPatronInCollections','note','query','string',NULL,FALSE)
+ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'collectionsRemovePatronFromCollections',
+ '/patron/:usrid/collections/:shortname',
+ 'delete',
+ 'Remove patron from collections.',
+ 'open-ils.collections',
+ 'open-ils.collections.remove_from_collections',
+ 'eg_auth_token param.usrid param.shortname'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES
+ ('collectionsRemovePatronFromCollections','usrid','path','integer',TRUE),
+ ('collectionsRemovePatronFromCollections','shortname','path','string',TRUE)
+ON CONFLICT DO NOTHING;
+
+
+------- courses
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('activeCourses', '/courses', 'get', 'Retrieve all courses used for course material reservation', 'OpenILS::OpenAPI::Controller::course', 'get_active_courses', 'eg_auth_token every_param.org')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES ('activeCourses','org','query','integer') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('activeCourses','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('activeRoles', '/courses/public_role_users', 'get', 'Retrieve all public roles used for courses', 'OpenILS::OpenAPI::Controller::course', 'get_all_course_public_roles', 'eg_auth_token every_param.org')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES ('activeRoles','org','query','integer') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('activeRoles','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrieveCourse', '/course/:id', 'get', 'Retrieve one detailed course', 'OpenILS::OpenAPI::Controller::course', 'get_course_detail', 'eg_auth_token param.id')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveCourse','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveCourse','acmc') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrieveCourseMaterials', '/course/:id/materials', 'get', 'Retrieve detailed materials for one course', 'OpenILS::OpenAPI::Controller::course', 'get_course_materials', 'param.id')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveCourseMaterials','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveCourseMaterials','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrieveCourseUsers', '/course/:id/public_role_users', 'get', 'Retrieve detailed user list for one course', 'open-ils.courses', 'open-ils.courses.course_users.retrieve', 'param.id')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveCourseUsers','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveCourseUsers','array','object') ON CONFLICT DO NOTHING;
+
+--------- put likely stock endpoints into sets --------
+
+INSERT INTO openapi.endpoint_set_endpoint_map (endpoint, endpoint_set)
+ SELECT e.operation_id, s.name FROM openapi.endpoint e JOIN openapi.endpoint_set s ON (e.path LIKE '/'||RTRIM(s.name,'s')||'%')
+ON CONFLICT DO NOTHING;
+
+-- Check that all endpoints are in sets -- should return 0 rows
+SELECT * FROM openapi.endpoint e WHERE NOT EXISTS (SELECT 1 FROM openapi.endpoint_set_endpoint_map WHERE endpoint = e.operation_id);
+
+-- Global Fieldmapper property-filtering org and user settings
+INSERT INTO config.settings_group (name,label) VALUES ('openapi','OpenAPI data access control');
+
+INSERT INTO config.org_unit_setting_type (name,label,grp) VALUES ('REST.api.blacklist_properties','Globally filtered Fieldmapper properties','openapi');
+INSERT INTO config.org_unit_setting_type (name,label,grp) VALUES ('REST.api.whitelist_properties','Globally whitelisted Fieldmapper properties','openapi');
+
+UPDATE config.org_unit_setting_type
+ SET update_perm = (SELECT id FROM permission.perm_list WHERE code = 'ADMIN_OPENAPI' LIMIT 1)
+ WHERE name IN ('REST.api.blacklist_properties','REST.api.whitelist_properties');
+
+INSERT INTO config.usr_setting_type (name,label,grp) VALUES ('REST.api.whitelist_properties','Globally whitelisted Fieldmapper properties','openapi');
+INSERT INTO config.usr_setting_type (name,label,grp) VALUES ('REST.api.blacklist_properties','Globally filtered Fieldmapper properties','openapi');
+
+
+/* -- Some extra example permission setup, to allow (basically) readonly patron retrieve
+
+INSERT INTO permission.perm_list (code,description) VALUES ('REST.api.patrons.detail.read','Permission meant to facilitate read-only patron-related API access');
+INSERT INTO openapi.perm_set (name) values ('Patron Detail, Readonly');
+INSERT INTO openapi.perm_set_perm_map (perm_set,perm) SELECT s.id, p.id FROM openapi.perm_set s, permission.perm_list p WHERE p.code IN ('REST.api', 'REST.api.patrons.detail.read') AND s.name='Patron RO';
+INSERT INTO openapi.endpoint_perm_set_map (endpoint,perm_set) SELECT 'retrievePatronProfile', s.id FROM openapi.perm_set s WHERE s.name='Patron RO';
+
+*/
+COMMIT;
+
+-- Update auditor tables to catch changes to source tables.
+-- Can be removed/skipped if there were no schema changes.
+SELECT auditor.update_auditors();
commit 75c301f7f8ef9f87e75903a6da1ec85e16876ee6
Author: Michele Morgan <mmorgan(a)noblenet.org>
Date: Tue Apr 15 16:39:49 2025 -0400
Forward-port 3.14.4-3.14.5 upgrade script
Signed-off-by: Michele Morgan <mmorgan(a)noblenet.org>
diff --git a/Open-ILS/src/sql/Pg/version-upgrade/3.14.4-3.14.5-upgrade-db.sql b/Open-ILS/src/sql/Pg/version-upgrade/3.14.4-3.14.5-upgrade-db.sql
new file mode 100644
index 0000000000..0671b7e5f0
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/version-upgrade/3.14.4-3.14.5-upgrade-db.sql
@@ -0,0 +1,9 @@
+--Upgrade Script for 3.14.4 to 3.14.5
+\set eg_version '''3.14.5'''
+BEGIN;
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('3.14.5', :eg_version);
+COMMIT;
+
+-- Update auditor tables to catch changes to source tables.
+-- Can be removed/skipped if there were no schema changes.
+SELECT auditor.update_auditors();
commit 70e4330d088144db4de6f15775dc4c10b16d8a36
Author: Michele Morgan <mmorgan(a)noblenet.org>
Date: Tue Apr 15 16:32:29 2025 -0400
Forward-port 3.13.9-3.13.10 upgrade script
Signed-off-by: Michele Morgan <mmorgan(a)noblenet.org>
diff --git a/Open-ILS/src/sql/Pg/version-upgrade/3.13.9-3.13.10-upgrade-db.sql b/Open-ILS/src/sql/Pg/version-upgrade/3.13.9-3.13.10-upgrade-db.sql
new file mode 100644
index 0000000000..3a86a7c8f9
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/version-upgrade/3.13.9-3.13.10-upgrade-db.sql
@@ -0,0 +1,9 @@
+--Upgrade Script for 3.13.9 to 3.13.10
+\set eg_version '''3.13.10'''
+BEGIN;
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('3.13.10', :eg_version);
+COMMIT;
+
+-- Update auditor tables to catch changes to source tables.
+-- Can be removed/skipped if there were no schema changes.
+SELECT auditor.update_auditors();
-----------------------------------------------------------------------
Summary of changes:
...pgrade-db.sql => 3.13.9-3.13.10-upgrade-db.sql} | 6 +-
...upgrade-db.sql => 3.14.4-3.14.5-upgrade-db.sql} | 6 +-
.../3.14.4-3.15.0-upgrade-db.sql} | 1242 +++++++++++++++++++-
3 files changed, 1247 insertions(+), 7 deletions(-)
copy Open-ILS/src/sql/Pg/version-upgrade/{3.12.5-3.12.6-upgrade-db.sql => 3.13.9-3.13.10-upgrade-db.sql} (72%)
copy Open-ILS/src/sql/Pg/version-upgrade/{3.12.5-3.12.6-upgrade-db.sql => 3.14.4-3.14.5-upgrade-db.sql} (72%)
copy Open-ILS/src/sql/Pg/{upgrade/1468.schema.openapi.sql => version-upgrade/3.14.4-3.15.0-upgrade-db.sql} (63%)
mode change 100755 => 100644
hooks/post-receive
--
Evergreen ILS
1
0

[GIT] Evergreen ILS branch tags/rel_3_15_0 updated. ceb33320c47c86837d91e075b0ad182d188025a1
by Git User 15 Apr '25
by Git User 15 Apr '25
15 Apr '25
This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".
The branch, tags/rel_3_15_0 has been updated
via ceb33320c47c86837d91e075b0ad182d188025a1 (commit)
from 95c389a1cdf7f4db252173b060f690c3a194a430 (commit)
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
- Log -----------------------------------------------------------------
commit ceb33320c47c86837d91e075b0ad182d188025a1
Author: Gina Monti <gmonti(a)biblio.org>
Date: Tue Apr 15 14:00:39 2025 -0400
Bumping version numbers and adding Upgrade Script
Signed-off-by: Gina Monti <gmonti(a)biblio.org>
diff --git a/Open-ILS/src/perlmods/lib/OpenILS.pm b/Open-ILS/src/perlmods/lib/OpenILS.pm
index d7235ce61a..3957011448 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS.pm
@@ -6,6 +6,6 @@ OpenILS - Client and server support for the Evergreen open source library system
=cut
-our $VERSION = '2.4';
+our $VERSION = '3.1500';
1;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application.pm
index 1c09bee84b..6635f74ed7 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application.pm
@@ -26,7 +26,7 @@ sub use_authoritative {
sub ils_version {
# version format is "x-y-z", for example "2-0-0" for Evergreen 2.0.0
# For branches, format is "x-y"
- return "HEAD";
+ return "3-15-0";
}
__PACKAGE__->register_method(
diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql
index 13d695aadf..87fb175040 100644
--- a/Open-ILS/src/sql/Pg/002.schema.config.sql
+++ b/Open-ILS/src/sql/Pg/002.schema.config.sql
@@ -93,6 +93,7 @@ CREATE TRIGGER no_overlapping_deps
FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates');
INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1468', :eg_version); -- miker/rdavis/jeffdavis/jeff
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('3.15.0', :eg_version);
CREATE TABLE config.bib_source (
id SERIAL PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/version-upgrade/3.14.4-3.15.0-upgrade-db.sql b/Open-ILS/src/sql/Pg/version-upgrade/3.14.4-3.15.0-upgrade-db.sql
new file mode 100644
index 0000000000..2dac5d2ca9
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/version-upgrade/3.14.4-3.15.0-upgrade-db.sql
@@ -0,0 +1,2712 @@
+--Upgrade Script for 3.14.4 to 3.15.0
+\set eg_version '''3.15.0'''
+BEGIN;
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('3.15.0', :eg_version);
+
+SELECT evergreen.upgrade_deps_block_check('1450', :eg_version);
+
+INSERT into config.workstation_setting_type
+ (name, grp, label, description, datatype)
+VALUES (
+ 'eg.admin.keyboard_shortcuts.disable_single',
+ 'gui',
+ oils_i18n_gettext('eg.admin.keyboard_shortcuts.disable_single',
+ 'Staff Client: disable single-letter keyboard shortcuts',
+ 'coust', 'label'),
+ oils_i18n_gettext('eg.admin.keyboard_shortcuts.disable_single',
+ 'Disables single-letter keyboard shortcuts if set to true. Screen reader users should set this to true to avoid interference with standard keyboard shortcuts.',
+ 'coust', 'description'),
+ 'bool'
+);
+
+
+SELECT evergreen.upgrade_deps_block_check('1452', :eg_version);
+
+INSERT INTO config.org_unit_setting_type
+ (name, label, description, grp, datatype)
+VALUES (
+ 'ui.staff.place_holds_for_recent_patrons',
+ oils_i18n_gettext(
+ 'ui.staff.place_holds_for_recent_patrons',
+ 'Place holds for recent patrons',
+ 'coust',
+ 'label'
+ ),
+ oils_i18n_gettext(
+ 'ui.staff.place_holds_for_recent_patrons',
+ 'Loading a patron in the place holds interface designates them as recent. ' ||
+ 'Show the interface to load recent patrons when placing holds.',
+ 'coust',
+ 'description'
+ ),
+ 'gui',
+ 'bool'
+);
+
+
+SELECT evergreen.upgrade_deps_block_check('1453', :eg_version);
+
+INSERT INTO permission.perm_list ( id, code, description ) SELECT DISTINCT
+ 676,
+ 'UPDATE_TOP_OF_QUEUE',
+ oils_i18n_gettext(676,
+ 'Allow setting and unsetting hold from top of hold queue (cut in line)', 'ppl', 'description'
+ )
+ FROM permission.perm_list
+ WHERE NOT EXISTS (SELECT 1 FROM permission.perm_list WHERE code = 'UPDATE_TOP_OF_QUEUE');
+
+
+--Assign permission to any perm groups with UPDATE_HOLD_REQUEST_TIME
+WITH perms_to_add AS
+ (SELECT id FROM
+ permission.perm_list
+ WHERE code IN ('UPDATE_TOP_OF_QUEUE'))
+INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
+ SELECT grp, perms_to_add.id as perm, depth, grantable
+ FROM perms_to_add,
+ permission.grp_perm_map
+
+ --- Don't add the permissions if they have already been assigned
+ WHERE grp NOT IN
+ (SELECT DISTINCT grp FROM permission.grp_perm_map
+ INNER JOIN perms_to_add ON perm=perms_to_add.id)
+
+ --- we're going to match the depth of their existing perm
+ AND perm = (
+ SELECT id
+ FROM permission.perm_list
+ WHERE code = 'UPDATE_HOLD_REQUEST_TIME'
+ );
+
+-- Add missing FROM clause entry to asset.opac_lasso_record_copy_count
+
+
+SELECT evergreen.upgrade_deps_block_check('1455', :eg_version);
+
+CREATE OR REPLACE FUNCTION asset.opac_lasso_record_copy_count (i_lasso INT, rid BIGINT) RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT) AS $f$
+DECLARE
+ ans RECORD;
+ trans INT;
+BEGIN
+ SELECT 1 INTO trans FROM biblio.record_entry b JOIN config.bib_source src ON (b.source = src.id) WHERE src.transcendant AND b.id = rid;
+
+ FOR ans IN SELECT u.org_unit AS id FROM actor.org_lasso_map AS u WHERE lasso = i_lasso LOOP
+ RETURN QUERY
+ WITH org_list AS (SELECT ARRAY_AGG(id)::BIGINT[] AS orgs FROM actor.org_unit_descendants(ans.id) x),
+ available_statuses AS (SELECT ARRAY_AGG(id) AS ids FROM config.copy_status WHERE is_available),
+ mask AS (SELECT c_attrs FROM asset.patron_default_visibility_mask() x)
+ SELECT -1,
+ ans.id,
+ COUNT( av.id ),
+ SUM( (cp.status = ANY (available_statuses.ids))::INT ),
+ COUNT( av.id ),
+ trans
+ FROM mask,
+ org_list,
+ available_statuses,
+ asset.copy_vis_attr_cache av
+ JOIN asset.copy cp ON (cp.id = av.target_copy AND av.record = rid)
+ WHERE cp.circ_lib = ANY (org_list.orgs) AND av.vis_attr_vector @@ mask.c_attrs::query_int
+ GROUP BY 1,2,6;
+
+ IF NOT FOUND THEN
+ RETURN QUERY SELECT -1, ans.id, 0::BIGINT, 0::BIGINT, 0::BIGINT, trans;
+ END IF;
+
+ END LOOP;
+
+ RETURN;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+
+SELECT evergreen.upgrade_deps_block_check('1456', :eg_version);
+
+INSERT into config.workstation_setting_type
+ (name, grp, label, description, datatype)
+VALUES (
+ 'ui.staff.grid.density',
+ 'gui',
+ oils_i18n_gettext('ui.staff.grid.density',
+ 'Grid UI density',
+ 'coust', 'label'),
+ oils_i18n_gettext('ui.staff.grid.density',
+ 'Whitespace around table cells in staff UI data grids. Default is "standard".',
+ 'coust', 'description'),
+ 'string'
+);
+
+
+-- Move OPAC alert banner feature from config file to a library setting
+
+SELECT evergreen.upgrade_deps_block_check('1458', :eg_version);
+
+INSERT into config.org_unit_setting_type
+ (name, grp, label, description, datatype)
+VALUES (
+ 'opac.alert_banner_show',
+ 'opac',
+ oils_i18n_gettext('opac.alert_banner_show',
+ 'OPAC Alert Banner: Display',
+ 'coust', 'label'),
+ oils_i18n_gettext('opac.alert_message_show',
+ 'Show an alert banner in the OPAC. Default is false.',
+ 'coust', 'description'),
+ 'bool'
+);
+
+INSERT into config.org_unit_setting_type
+ (name, grp, label, description, datatype)
+VALUES (
+ 'opac.alert_banner_type',
+ 'opac',
+ oils_i18n_gettext('opac.alert_banner_type',
+ 'OPAC Alert Banner: Type',
+ 'coust', 'label'),
+ oils_i18n_gettext('opac.alert_message_type',
+ 'Determine the display of the banner. Options are: success, info, warning, danger.',
+ 'coust', 'description'),
+ 'string'
+);
+
+INSERT into config.org_unit_setting_type
+ (name, grp, label, description, datatype)
+VALUES (
+ 'opac.alert_banner_text',
+ 'opac',
+ oils_i18n_gettext('opac.alert_banner_text',
+ 'OPAC Alert Banner: Text',
+ 'coust', 'label'),
+ oils_i18n_gettext('opac.alert_message_text',
+ 'Text that will display in the alert banner.',
+ 'coust', 'description'),
+ 'string'
+);
+
+
+-- Move Google Analytics settings from config.tt2 to library settings
+
+SELECT evergreen.upgrade_deps_block_check('1459', :eg_version);
+
+INSERT into config.org_unit_setting_type
+ (name, grp, label, description, datatype)
+VALUES (
+ 'opac.google_analytics_enable',
+ 'opac',
+ oils_i18n_gettext('opac.google_analytics_enable',
+ 'Google Analytics: Enable',
+ 'coust', 'label'),
+ oils_i18n_gettext('opac.alert_message_show',
+ 'Enable Google Analytics in the OPAC. Default is false.',
+ 'coust', 'description'),
+ 'bool'
+);
+
+INSERT into config.org_unit_setting_type
+ (name, grp, label, description, datatype)
+VALUES (
+ 'opac.google_analytics_code',
+ 'opac',
+ oils_i18n_gettext('opac.google_analytics_code',
+ 'Google Analytics: Code',
+ 'coust', 'label'),
+ oils_i18n_gettext('opac.google_analytics_code',
+ 'Account code provided by Google. (Example: G-GVGQ11X12)',
+ 'coust', 'description'),
+ 'string'
+);
+
+
+SELECT evergreen.upgrade_deps_block_check('1460', :eg_version); -- JBoyer
+
+-- Note: the value will not be consistent from system to system. It's an opaque key that has no meaning of its own,
+-- if the value cached in the client does not match or is missing, it clears some cached values and then saves the current value.
+INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
+ 'staff.client_cache_key',
+ oils_i18n_gettext(
+ 'staff.client_cache_key',
+ 'Change this value to force staff clients to clear some cached values',
+ 'cgf',
+ 'label'
+ ),
+ md5(random()::text),
+ TRUE
+);
+
+
+SELECT evergreen.upgrade_deps_block_check('1461', :eg_version);
+
+-- for searching e.g. "111-111-1111"
+CREATE INDEX actor_usr_setting_phone_values_idx
+ ON actor.usr_setting (evergreen.lowercase(value))
+ WHERE name IN ('opac.default_phone', 'opac.default_sms_notify');
+
+-- for searching e.g. "1111111111"
+CREATE INDEX actor_usr_setting_phone_values_numeric_idx
+ ON actor.usr_setting (evergreen.lowercase(REGEXP_REPLACE(value, '[^0-9]', '', 'g')))
+ WHERE name IN ('opac.default_phone', 'opac.default_sms_notify');
+
+
+SELECT evergreen.upgrade_deps_block_check('1462', :eg_version);
+
+INSERT INTO acq.edi_attr (key, label) VALUES
+ ('LINEITEM_SEQUENTIAL_ID',
+ oils_i18n_gettext('LINEITEM_SEQUENTIAL_ID',
+ 'Lineitems Are Enumerated Sequentially', 'aea', 'label'));
+
+
+SELECT evergreen.upgrade_deps_block_check('1463', :eg_version);
+
+INSERT INTO config.org_unit_setting_type
+ (grp, name, datatype, label, description)
+VALUES (
+ 'gui',
+ 'ui.cat.volume_copy_editor.template_bar.show_save_template', 'bool',
+ oils_i18n_gettext(
+ 'ui.cat.volume_copy_editor.template_bar.show_save_template',
+ 'Show "Save Template" in Holdings Editor',
+ 'coust',
+ 'label'
+ ),
+ oils_i18n_gettext(
+ 'ui.cat.volume_copy_editor.template_bar.show_save_template',
+ 'Displays the "Save Template" button for the template bar in the Volume/Copy/Holdings Editor. By default, this is only displayed when working with templates from the Admin interface.',
+ 'coust',
+ 'description'
+ )
+);
+
+INSERT INTO config.org_unit_setting_type
+ (grp, name, datatype, label, description)
+VALUES (
+ 'gui',
+ 'ui.cat.volume_copy_editor.hide_template_bar', 'bool',
+ oils_i18n_gettext(
+ 'ui.cat.volume_copy_editor.hide_template_bar',
+ 'Hide the entire template bar in Holdings Editor',
+ 'coust',
+ 'label'
+ ),
+ oils_i18n_gettext(
+ 'ui.cat.volume_copy_editor.hide_template_bar',
+ 'Hides the template bar in the Volume/Copy/Holdings Editor. By default, the template bar is displayed in this interface.',
+ 'coust',
+ 'description'
+ )
+);
+
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+ 'eg.grid.cat.volcopy.template_grid', 'gui', 'object',
+ oils_i18n_gettext(
+ 'eg.grid.cat.volcopy.template_grid',
+ 'Holdings Template Grid Settings',
+ 'cwst', 'label'
+ )
+);
+
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+ 'eg.grid.holdings.copy_tags.tag_map_list', 'gui', 'object',
+ oils_i18n_gettext(
+ 'eg.grid.holdings.copy_tags.tag_map_list',
+ 'Copy Tag Maps Template Grid Settings',
+ 'cwst', 'label'
+ )
+);
+
+
+SELECT evergreen.upgrade_deps_block_check('1464', :eg_version);
+
+-- If you want to know how many items are available in a particular library group,
+-- you can't easily sum the results of, say, asset.staff_lasso_record_copy_count,
+-- since library groups can theoretically include descendants of other org units
+-- in the library group (for example, the group could include a system and a branch
+-- within that same system), which means that certain items would be counted twice.
+-- The following functions address that problem by providing deduplicated sums that
+-- only count each item once.
+
+CREATE OR REPLACE FUNCTION asset.staff_lasso_record_copy_count_sum(lasso_id INT, record_id BIGINT)
+ RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT, library_group INT) AS $$
+ BEGIN
+ IF (lasso_id IS NULL) THEN RETURN; END IF;
+ IF (record_id IS NULL) THEN RETURN; END IF;
+ RETURN QUERY SELECT
+ -1,
+ -1,
+ COUNT(cp.id),
+ SUM( CASE WHEN cp.status IN (SELECT id FROM config.copy_status WHERE holdable AND is_available) THEN 1 ELSE 0 END ),
+ SUM( CASE WHEN cl.opac_visible AND cp.opac_visible THEN 1 ELSE 0 END),
+ 0,
+ lasso_id
+ FROM ( SELECT DISTINCT descendants.id FROM actor.org_lasso_map aolmp JOIN LATERAL actor.org_unit_descendants(aolmp.org_unit) AS descendants ON TRUE WHERE aolmp.lasso = lasso_id) d
+ JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
+ JOIN asset.copy_location cl ON (cp.location = cl.id AND NOT cl.deleted)
+ JOIN asset.call_number cn ON (cn.record = record_id AND cn.id = cp.call_number AND NOT cn.deleted);
+ END;
+$$ LANGUAGE PLPGSQL STABLE ROWS 1;
+
+CREATE OR REPLACE FUNCTION asset.opac_lasso_record_copy_count_sum(lasso_id INT, record_id BIGINT)
+ RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT, library_group INT) AS $$
+ BEGIN
+ IF (lasso_id IS NULL) THEN RETURN; END IF;
+ IF (record_id IS NULL) THEN RETURN; END IF;
+
+ RETURN QUERY SELECT
+ -1,
+ -1,
+ COUNT(cp.id),
+ SUM( CASE WHEN cp.status IN (SELECT id FROM config.copy_status WHERE holdable AND is_available) THEN 1 ELSE 0 END ),
+ SUM( CASE WHEN cl.opac_visible AND cp.opac_visible THEN 1 ELSE 0 END),
+ 0,
+ lasso_id
+ FROM ( SELECT DISTINCT descendants.id FROM actor.org_lasso_map aolmp JOIN LATERAL actor.org_unit_descendants(aolmp.org_unit) AS descendants ON TRUE WHERE aolmp.lasso = lasso_id) d
+ JOIN asset.copy cp ON (cp.circ_lib = d.id AND NOT cp.deleted)
+ JOIN asset.copy_location cl ON (cp.location = cl.id AND NOT cl.deleted)
+ JOIN asset.call_number cn ON (cn.record = record_id AND cn.id = cp.call_number AND NOT cn.deleted)
+ JOIN asset.copy_vis_attr_cache av ON (cp.id = av.target_copy AND av.record = record_id)
+ JOIN LATERAL (SELECT c_attrs FROM asset.patron_default_visibility_mask()) AS mask ON TRUE
+ WHERE av.vis_attr_vector @@ mask.c_attrs::query_int;
+ END;
+$$ LANGUAGE PLPGSQL STABLE ROWS 1;
+
+CREATE OR REPLACE FUNCTION asset.staff_lasso_metarecord_copy_count_sum(lasso_id INT, metarecord_id BIGINT)
+ RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT, library_group INT) AS $$
+ SELECT (
+ -1,
+ -1,
+ SUM(sums.visible)::bigint,
+ SUM(sums.available)::bigint,
+ SUM(sums.unshadow)::bigint,
+ MIN(sums.transcendant),
+ lasso_id
+ ) FROM metabib.metarecord_source_map mmsm
+ JOIN LATERAL (SELECT visible, available, unshadow, transcendant FROM asset.staff_lasso_record_copy_count_sum(lasso_id, mmsm.source)) sums ON TRUE
+ WHERE mmsm.metarecord = metarecord_id;
+$$ LANGUAGE SQL STABLE ROWS 1;
+
+CREATE OR REPLACE FUNCTION asset.opac_lasso_metarecord_copy_count_sum(lasso_id INT, metarecord_id BIGINT)
+ RETURNS TABLE (depth INT, org_unit INT, visible BIGINT, available BIGINT, unshadow BIGINT, transcendant INT, library_group INT) AS $$
+ SELECT (
+ -1,
+ -1,
+ SUM(sums.visible)::bigint,
+ SUM(sums.available)::bigint,
+ SUM(sums.unshadow)::bigint,
+ MIN(sums.transcendant),
+ lasso_id
+ ) FROM metabib.metarecord_source_map mmsm
+ JOIN LATERAL (SELECT visible, available, unshadow, transcendant FROM asset.opac_lasso_record_copy_count_sum(lasso_id, mmsm.source)) sums ON TRUE
+ WHERE mmsm.metarecord = metarecord_id;
+$$ LANGUAGE SQL STABLE ROWS 1;
+
+
+CREATE OR REPLACE FUNCTION asset.copy_org_ids(org_units INT[], depth INT, library_groups INT[])
+RETURNS TABLE (id INT)
+AS $$
+DECLARE
+ ancestor INT;
+BEGIN
+ RETURN QUERY SELECT org_unit FROM actor.org_lasso_map WHERE lasso = ANY(library_groups);
+ FOR ancestor IN SELECT unnest(org_units)
+ LOOP
+ RETURN QUERY
+ SELECT d.id
+ FROM actor.org_unit_descendants(ancestor, depth) d;
+ END LOOP;
+ RETURN;
+END;
+$$ LANGUAGE plpgsql;
+
+
+CREATE OR REPLACE FUNCTION asset.staff_copy_total(rec_id INT, org_units INT[], depth INT, library_groups INT[])
+RETURNS INT AS $$
+ SELECT COUNT(cp.id) total
+ FROM asset.copy cp
+ INNER JOIN asset.call_number cn ON (cn.id = cp.call_number AND NOT cn.deleted AND cn.record = rec_id)
+ INNER JOIN asset.copy_location cl ON (cp.location = cl.id AND NOT cl.deleted)
+ WHERE cp.circ_lib = ANY (SELECT asset.copy_org_ids(org_units, depth, library_groups))
+ AND NOT cp.deleted;
+$$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION asset.opac_copy_total(rec_id INT, org_units INT[], depth INT, library_groups INT[])
+RETURNS INT AS $$
+ SELECT COUNT(cp.id) total
+ FROM asset.copy cp
+ INNER JOIN asset.call_number cn ON (cn.id = cp.call_number AND NOT cn.deleted AND cn.record = rec_id)
+ INNER JOIN asset.copy_location cl ON (cp.location = cl.id AND NOT cl.deleted)
+ INNER JOIN asset.copy_vis_attr_cache av ON (cp.id = av.target_copy AND av.record = rec_id)
+ JOIN LATERAL (SELECT c_attrs FROM asset.patron_default_visibility_mask()) AS mask ON TRUE
+ WHERE av.vis_attr_vector @@ mask.c_attrs::query_int
+ AND cp.circ_lib = ANY (SELECT asset.copy_org_ids(org_units, depth, library_groups))
+ AND NOT cp.deleted;
+$$ LANGUAGE SQL;
+
+-- We are adding another argument, which means that we must delete functions with the old signature
+DROP FUNCTION IF EXISTS unapi.holdings_xml(
+ bid BIGINT,
+ ouid INT,
+ org TEXT,
+ depth INT,
+ includes TEXT[],
+ slimit HSTORE,
+ soffset HSTORE,
+ include_xmlns BOOL,
+ pref_lib INT);
+
+CREATE OR REPLACE FUNCTION unapi.holdings_xml (
+ bid BIGINT,
+ ouid INT,
+ org TEXT,
+ depth INT DEFAULT NULL,
+ includes TEXT[] DEFAULT NULL::TEXT[],
+ slimit HSTORE DEFAULT NULL,
+ soffset HSTORE DEFAULT NULL,
+ include_xmlns BOOL DEFAULT TRUE,
+ pref_lib INT DEFAULT NULL,
+ library_group INT DEFAULT NULL
+)
+RETURNS XML AS $F$
+ SELECT XMLELEMENT(
+ name holdings,
+ XMLATTRIBUTES(
+ CASE WHEN $8 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+ CASE WHEN ('bre' = ANY ($5)) THEN 'tag:open-ils.org:U2@bre/' || $1 || '/' || $3 ELSE NULL END AS id,
+ (SELECT record_has_holdable_copy FROM asset.record_has_holdable_copy($1)) AS has_holdable
+ ),
+ XMLELEMENT(
+ name counts,
+ (SELECT XMLAGG(XMLELEMENT::XML) FROM (
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('public' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.opac_ou_record_copy_count($2, $1)
+ UNION
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('staff' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.staff_ou_record_copy_count($2, $1)
+ UNION
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('pref_lib' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.opac_ou_record_copy_count($9, $1)
+ UNION
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('public' as type, depth, library_group, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.opac_lasso_record_copy_count_sum(library_group, $1)
+ UNION
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('staff' as type, depth, library_group, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.staff_lasso_record_copy_count_sum(library_group, $1)
+ ORDER BY 1
+ )x)
+ ),
+ CASE
+ WHEN ('bmp' = ANY ($5)) THEN
+ XMLELEMENT(
+ name monograph_parts,
+ (SELECT XMLAGG(bmp) FROM (
+ SELECT unapi.bmp( id, 'xml', 'monograph_part', array_remove( array_remove($5,'bre'), 'holdings_xml'), $3, $4, $6, $7, FALSE)
+ FROM biblio.monograph_part
+ WHERE NOT deleted AND record = $1
+ )x)
+ )
+ ELSE NULL
+ END,
+ XMLELEMENT(
+ name volumes,
+ (SELECT XMLAGG(acn ORDER BY rank, name, label_sortkey) FROM (
+ -- Physical copies
+ SELECT unapi.acn(y.id,'xml','volume',array_remove( array_remove($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE), y.rank, name, label_sortkey
+ FROM evergreen.ranked_volumes($1, $2, $4, $6, $7, $9, $5) AS y
+ UNION ALL
+ -- Located URIs
+ SELECT unapi.acn(uris.id,'xml','volume',array_remove( array_remove($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE), uris.rank, name, label_sortkey
+ FROM evergreen.located_uris($1, $2, $9) AS uris
+ )x)
+ ),
+ CASE WHEN ('ssub' = ANY ($5)) THEN
+ XMLELEMENT(
+ name subscriptions,
+ (SELECT XMLAGG(ssub) FROM (
+ SELECT unapi.ssub(id,'xml','subscription','{}'::TEXT[], $3, $4, $6, $7, FALSE)
+ FROM serial.subscription
+ WHERE record_entry = $1
+ )x)
+ )
+ ELSE NULL END,
+ CASE WHEN ('acp' = ANY ($5)) THEN
+ XMLELEMENT(
+ name foreign_copies,
+ (SELECT XMLAGG(acp) FROM (
+ SELECT unapi.acp(p.target_copy,'xml','copy',array_remove($5,'acp'), $3, $4, $6, $7, FALSE)
+ FROM biblio.peer_bib_copy_map p
+ JOIN asset.copy c ON (p.target_copy = c.id)
+ WHERE NOT c.deleted AND p.peer_record = $1
+ LIMIT ($6 -> 'acp')::INT
+ OFFSET ($7 -> 'acp')::INT
+ )x)
+ )
+ ELSE NULL END
+ );
+$F$ LANGUAGE SQL STABLE;
+
+DROP FUNCTION IF EXISTS unapi.bre (
+ obj_id BIGINT,
+ format TEXT,
+ ename TEXT,
+ includes TEXT[],
+ org TEXT,
+ depth INT,
+ slimit HSTORE,
+ soffset HSTORE,
+ include_xmlns BOOL,
+ pref_lib INT);
+
+CREATE OR REPLACE FUNCTION unapi.bre (
+ obj_id BIGINT,
+ format TEXT,
+ ename TEXT,
+ includes TEXT[],
+ org TEXT,
+ depth INT DEFAULT NULL,
+ slimit HSTORE DEFAULT NULL,
+ soffset HSTORE DEFAULT NULL,
+ include_xmlns BOOL DEFAULT TRUE,
+ pref_lib INT DEFAULT NULL,
+ library_group INT DEFAULT NULL
+)
+RETURNS XML AS $F$
+DECLARE
+ me biblio.record_entry%ROWTYPE;
+ layout unapi.bre_output_layout%ROWTYPE;
+ xfrm config.xml_transform%ROWTYPE;
+ ouid INT;
+ tmp_xml TEXT;
+ top_el TEXT;
+ output XML;
+ hxml XML;
+ axml XML;
+ source XML;
+BEGIN
+
+ IF org = '-' OR org IS NULL THEN
+ SELECT shortname INTO org FROM evergreen.org_top();
+ END IF;
+
+ SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
+
+ IF ouid IS NULL THEN
+ RETURN NULL::XML;
+ END IF;
+
+ IF format = 'holdings_xml' THEN -- the special case
+ output := unapi.holdings_xml( obj_id, ouid, org, depth, includes, slimit, soffset, include_xmlns, NULL, library_group);
+ RETURN output;
+ END IF;
+
+ SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
+
+ IF layout.name IS NULL THEN
+ RETURN NULL::XML;
+ END IF;
+
+ SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform;
+
+ SELECT * INTO me FROM biblio.record_entry WHERE id = obj_id;
+
+ -- grab bib_source, if any
+ IF ('cbs' = ANY (includes) AND me.source IS NOT NULL) THEN
+ source := unapi.cbs(me.source,NULL,NULL,NULL,NULL);
+ ELSE
+ source := NULL::XML;
+ END IF;
+
+ -- grab SVF if we need them
+ IF ('mra' = ANY (includes)) THEN
+ axml := unapi.mra(obj_id,NULL,NULL,NULL,NULL);
+ ELSE
+ axml := NULL::XML;
+ END IF;
+
+ -- grab holdings if we need them
+ IF ('holdings_xml' = ANY (includes)) THEN
+ hxml := unapi.holdings_xml(obj_id, ouid, org, depth, array_remove(includes,'holdings_xml'), slimit, soffset, include_xmlns, pref_lib, library_group);
+ ELSE
+ hxml := NULL::XML;
+ END IF;
+
+
+ -- generate our item node
+
+
+ IF format = 'marcxml' THEN
+ tmp_xml := me.marc;
+ IF tmp_xml !~ E'<marc:' THEN -- If we're not using the prefixed namespace in this record, then remove all declarations of it
+ tmp_xml := REGEXP_REPLACE(tmp_xml, ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g');
+ END IF;
+ ELSE
+ tmp_xml := oils_xslt_process(me.marc, xfrm.xslt)::XML;
+ END IF;
+
+ top_el := REGEXP_REPLACE(tmp_xml, E'^.*?<((?:\\S+:)?' || layout.holdings_element || ').*$', E'\\1');
+
+ IF source IS NOT NULL THEN
+ tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', source || '</' || top_el || E'>\\1');
+ END IF;
+
+ IF axml IS NOT NULL THEN
+ tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', axml || '</' || top_el || E'>\\1');
+ END IF;
+
+ IF hxml IS NOT NULL THEN -- XXX how do we configure the holdings position?
+ tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1');
+ END IF;
+
+ IF ('bre.unapi' = ANY (includes)) THEN
+ output := REGEXP_REPLACE(
+ tmp_xml,
+ '</' || top_el || '>(.*?)',
+ XMLELEMENT(
+ name abbr,
+ XMLATTRIBUTES(
+ 'http://www.w3.org/1999/xhtml' AS xmlns,
+ 'unapi-id' AS class,
+ 'tag:open-ils.org:U2@bre/' || obj_id || '/' || org AS title
+ )
+ )::TEXT || '</' || top_el || E'>\\1'
+ );
+ ELSE
+ output := tmp_xml;
+ END IF;
+
+ IF ('bre.extern' = ANY (includes)) THEN
+ output := REGEXP_REPLACE(
+ tmp_xml,
+ '</' || top_el || '>(.*?)',
+ XMLELEMENT(
+ name extern,
+ XMLATTRIBUTES(
+ 'http://open-ils.org/spec/biblio/v1' AS xmlns,
+ me.creator AS creator,
+ me.editor AS editor,
+ me.create_date AS create_date,
+ me.edit_date AS edit_date,
+ me.quality AS quality,
+ me.fingerprint AS fingerprint,
+ me.tcn_source AS tcn_source,
+ me.tcn_value AS tcn_value,
+ me.owner AS owner,
+ me.share_depth AS share_depth,
+ me.active AS active,
+ me.deleted AS deleted
+ )
+ )::TEXT || '</' || top_el || E'>\\1'
+ );
+ ELSE
+ output := tmp_xml;
+ END IF;
+
+ output := REGEXP_REPLACE(output::TEXT,E'>\\s+<','><','gs')::XML;
+ RETURN output;
+END;
+$F$ LANGUAGE PLPGSQL STABLE;
+
+DROP FUNCTION IF EXISTS unapi.biblio_record_entry_feed (
+ id_list BIGINT[],
+ format TEXT,
+ includes TEXT[],
+ org TEXT,
+ depth INT,
+ slimit HSTORE,
+ soffset HSTORE,
+ include_xmlns BOOL,
+ title TEXT,
+ description TEXT,
+ creator TEXT,
+ update_ts TEXT,
+ unapi_url TEXT,
+ header_xml XML,
+ pref_lib INT);
+CREATE OR REPLACE FUNCTION unapi.biblio_record_entry_feed (
+ id_list BIGINT[],
+ format TEXT,
+ includes TEXT[],
+ org TEXT,
+ depth INT DEFAULT NULL,
+ slimit HSTORE DEFAULT NULL,
+ soffset HSTORE DEFAULT NULL,
+ include_xmlns BOOL DEFAULT TRUE,
+ title TEXT DEFAULT NULL,
+ description TEXT DEFAULT NULL,
+ creator TEXT DEFAULT NULL,
+ update_ts TEXT DEFAULT NULL,
+ unapi_url TEXT DEFAULT NULL,
+ header_xml XML DEFAULT NULL,
+ pref_lib INT DEFAULT NULL,
+ library_group INT DEFAULT NULL
+ ) RETURNS XML AS $F$
+DECLARE
+ layout unapi.bre_output_layout%ROWTYPE;
+ transform config.xml_transform%ROWTYPE;
+ item_format TEXT;
+ tmp_xml TEXT;
+ xmlns_uri TEXT := 'http://open-ils.org/spec/feed-xml/v1';
+ ouid INT;
+ element_list TEXT[];
+BEGIN
+
+ IF org = '-' OR org IS NULL THEN
+ SELECT shortname INTO org FROM evergreen.org_top();
+ END IF;
+
+ SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
+ SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
+
+ IF layout.name IS NULL THEN
+ RETURN NULL::XML;
+ END IF;
+
+ SELECT * INTO transform FROM config.xml_transform WHERE name = layout.transform;
+ xmlns_uri := COALESCE(transform.namespace_uri,xmlns_uri);
+
+ -- Gather the bib xml
+ SELECT XMLAGG( unapi.bre(i, format, '', includes, org, depth, slimit, soffset, include_xmlns, pref_lib, library_group)) INTO tmp_xml FROM UNNEST( id_list ) i;
+
+ IF layout.title_element IS NOT NULL THEN
+ EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.title_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, title;
+ END IF;
+
+ IF layout.description_element IS NOT NULL THEN
+ EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.description_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, description;
+ END IF;
+
+ IF layout.creator_element IS NOT NULL THEN
+ EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.creator_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, creator;
+ END IF;
+
+ IF layout.update_ts_element IS NOT NULL THEN
+ EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.update_ts_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, update_ts;
+ END IF;
+
+ IF unapi_url IS NOT NULL THEN
+ EXECUTE $$SELECT XMLCONCAT( XMLELEMENT( name link, XMLATTRIBUTES( 'http://www.w3.org/1999/xhtml' AS xmlns, 'unapi-server' AS rel, $1 AS href, 'unapi' AS title)), $2)$$ INTO tmp_xml USING unapi_url, tmp_xml::XML;
+ END IF;
+
+ IF header_xml IS NOT NULL THEN tmp_xml := XMLCONCAT(header_xml,tmp_xml::XML); END IF;
+
+ element_list := regexp_split_to_array(layout.feed_top,E'\\.');
+ FOR i IN REVERSE ARRAY_UPPER(element_list, 1) .. 1 LOOP
+ EXECUTE 'SELECT XMLELEMENT( name '|| quote_ident(element_list[i]) ||', XMLATTRIBUTES( $1 AS xmlns), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML;
+ END LOOP;
+
+ RETURN tmp_xml::XML;
+END;
+$F$ LANGUAGE PLPGSQL STABLE;
+
+
+DROP FUNCTION IF EXISTS unapi.metabib_virtual_record_feed (
+ id_list BIGINT[],
+ format TEXT,
+ includes TEXT[],
+ org TEXT,
+ depth INT,
+ slimit HSTORE,
+ soffset HSTORE,
+ include_xmlns BOOL, title TEXT,
+ description TEXT,
+ creator TEXT,
+ update_ts TEXT,
+ unapi_url TEXT,
+ header_xml XML,
+ pref_lib INT);
+CREATE OR REPLACE FUNCTION unapi.metabib_virtual_record_feed (
+ id_list BIGINT[],
+ format TEXT,
+ includes TEXT[],
+ org TEXT,
+ depth INT DEFAULT NULL,
+ slimit HSTORE DEFAULT NULL,
+ soffset HSTORE DEFAULT NULL,
+ include_xmlns BOOL DEFAULT TRUE,
+ title TEXT DEFAULT NULL,
+ description TEXT DEFAULT NULL,
+ creator TEXT DEFAULT NULL,
+ update_ts TEXT DEFAULT NULL,
+ unapi_url TEXT DEFAULT NULL,
+ header_xml XML DEFAULT NULL,
+ pref_lib INT DEFAULT NULL,
+ library_group INT DEFAULT NULL ) RETURNS XML AS $F$
+DECLARE
+ layout unapi.bre_output_layout%ROWTYPE;
+ transform config.xml_transform%ROWTYPE;
+ item_format TEXT;
+ tmp_xml TEXT;
+ xmlns_uri TEXT := 'http://open-ils.org/spec/feed-xml/v1';
+ ouid INT;
+ element_list TEXT[];
+BEGIN
+
+ IF org = '-' OR org IS NULL THEN
+ SELECT shortname INTO org FROM evergreen.org_top();
+ END IF;
+
+ SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
+ SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
+
+ IF layout.name IS NULL THEN
+ RETURN NULL::XML;
+ END IF;
+
+ SELECT * INTO transform FROM config.xml_transform WHERE name = layout.transform;
+ xmlns_uri := COALESCE(transform.namespace_uri,xmlns_uri);
+
+ -- Gather the bib xml
+ SELECT XMLAGG( unapi.mmr(i, format, '', includes, org, depth, slimit, soffset, include_xmlns, pref_lib, library_group)) INTO tmp_xml FROM UNNEST( id_list ) i;
+
+ IF layout.title_element IS NOT NULL THEN
+ EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.title_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, title;
+ END IF;
+
+ IF layout.description_element IS NOT NULL THEN
+ EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.description_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, description;
+ END IF;
+
+ IF layout.creator_element IS NOT NULL THEN
+ EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.creator_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, creator;
+ END IF;
+
+ IF layout.update_ts_element IS NOT NULL THEN
+ EXECUTE 'SELECT XMLCONCAT( XMLELEMENT( name '|| layout.update_ts_element ||', XMLATTRIBUTES( $1 AS xmlns), $3), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML, update_ts;
+ END IF;
+
+ IF unapi_url IS NOT NULL THEN
+ EXECUTE $$SELECT XMLCONCAT( XMLELEMENT( name link, XMLATTRIBUTES( 'http://www.w3.org/1999/xhtml' AS xmlns, 'unapi-server' AS rel, $1 AS href, 'unapi' AS title)), $2)$$ INTO tmp_xml USING unapi_url, tmp_xml::XML;
+ END IF;
+
+ IF header_xml IS NOT NULL THEN tmp_xml := XMLCONCAT(header_xml,tmp_xml::XML); END IF;
+
+ element_list := regexp_split_to_array(layout.feed_top,E'\\.');
+ FOR i IN REVERSE ARRAY_UPPER(element_list, 1) .. 1 LOOP
+ EXECUTE 'SELECT XMLELEMENT( name '|| quote_ident(element_list[i]) ||', XMLATTRIBUTES( $1 AS xmlns), $2)' INTO tmp_xml USING xmlns_uri, tmp_xml::XML;
+ END LOOP;
+
+ RETURN tmp_xml::XML;
+END;
+$F$ LANGUAGE PLPGSQL STABLE;
+
+
+DROP FUNCTION IF EXISTS unapi.mmr(
+ obj_id BIGINT,
+ format TEXT,
+ ename TEXT,
+ includes TEXT[],
+ org TEXT,
+ depth INT,
+ slimit HSTORE,
+ soffset HSTORE,
+ include_xmlns BOOL,
+ pref_lib INT
+);
+
+CREATE OR REPLACE FUNCTION unapi.mmr (
+ obj_id BIGINT,
+ format TEXT,
+ ename TEXT,
+ includes TEXT[],
+ org TEXT,
+ depth INT DEFAULT NULL,
+ slimit HSTORE DEFAULT NULL,
+ soffset HSTORE DEFAULT NULL,
+ include_xmlns BOOL DEFAULT TRUE,
+ pref_lib INT DEFAULT NULL,
+ library_group INT DEFAULT NULL
+)
+RETURNS XML AS $F$
+DECLARE
+ mmrec metabib.metarecord%ROWTYPE;
+ leadrec biblio.record_entry%ROWTYPE;
+ subrec biblio.record_entry%ROWTYPE;
+ layout unapi.bre_output_layout%ROWTYPE;
+ xfrm config.xml_transform%ROWTYPE;
+ ouid INT;
+ xml_buf TEXT; -- growing XML document
+ tmp_xml TEXT; -- single-use XML string
+ xml_frag TEXT; -- single-use XML fragment
+ top_el TEXT;
+ output XML;
+ hxml XML;
+ axml XML;
+ subxml XML; -- subordinate records elements
+ sub_xpath TEXT;
+ parts TEXT[];
+BEGIN
+
+ -- xpath for extracting bre.marc values from subordinate records
+ -- so they may be appended to the MARC of the master record prior
+ -- to XSLT processing.
+ -- subjects, isbn, issn, upc -- anything else?
+ sub_xpath :=
+ '//*[starts-with(@tag, "6") or @tag="020" or @tag="022" or @tag="024"]';
+
+ IF org = '-' OR org IS NULL THEN
+ SELECT shortname INTO org FROM evergreen.org_top();
+ END IF;
+
+ SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
+
+ IF ouid IS NULL THEN
+ RETURN NULL::XML;
+ END IF;
+
+ SELECT INTO mmrec * FROM metabib.metarecord WHERE id = obj_id;
+ IF NOT FOUND THEN
+ RETURN NULL::XML;
+ END IF;
+
+ -- TODO: aggregate holdings from constituent records
+ IF format = 'holdings_xml' THEN -- the special case
+ output := unapi.mmr_holdings_xml(
+ obj_id, ouid, org, depth,
+ array_remove(includes,'holdings_xml'),
+ slimit, soffset, include_xmlns, pref_lib, library_group);
+ RETURN output;
+ END IF;
+
+ SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
+
+ IF layout.name IS NULL THEN
+ RETURN NULL::XML;
+ END IF;
+
+ SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform;
+
+ SELECT INTO leadrec * FROM biblio.record_entry WHERE id = mmrec.master_record;
+
+ -- Grab distinct MVF for all records if requested
+ IF ('mra' = ANY (includes)) THEN
+ axml := unapi.mmr_mra(obj_id,NULL,NULL,NULL,org,depth,NULL,NULL,TRUE,pref_lib);
+ ELSE
+ axml := NULL::XML;
+ END IF;
+
+ xml_buf = leadrec.marc;
+
+ hxml := NULL::XML;
+ IF ('holdings_xml' = ANY (includes)) THEN
+ hxml := unapi.mmr_holdings_xml(
+ obj_id, ouid, org, depth,
+ array_remove(includes,'holdings_xml'),
+ slimit, soffset, include_xmlns, pref_lib, library_group);
+ END IF;
+
+ subxml := NULL::XML;
+ parts := '{}'::TEXT[];
+ FOR subrec IN SELECT bre.* FROM biblio.record_entry bre
+ JOIN metabib.metarecord_source_map mmsm ON (mmsm.source = bre.id)
+ JOIN metabib.metarecord mmr ON (mmr.id = mmsm.metarecord)
+ WHERE mmr.id = obj_id AND NOT bre.deleted
+ ORDER BY CASE WHEN bre.id = mmr.master_record THEN 0 ELSE bre.id END
+ LIMIT COALESCE((slimit->'bre')::INT, 5) LOOP
+
+ IF subrec.id = leadrec.id THEN CONTINUE; END IF;
+ -- Append choice data from the the non-lead records to the
+ -- the lead record document
+
+ parts := parts || xpath(sub_xpath, subrec.marc::XML)::TEXT[];
+ END LOOP;
+
+ SELECT STRING_AGG( DISTINCT p , '' )::XML INTO subxml FROM UNNEST(parts) p;
+
+ -- append data from the subordinate records to the
+ -- main record document before applying the XSLT
+
+ IF subxml IS NOT NULL THEN
+ xml_buf := REGEXP_REPLACE(xml_buf,
+ '</record>(.*?)$', subxml || '</record>' || E'\\1');
+ END IF;
+
+ IF format = 'marcxml' THEN
+ -- If we're not using the prefixed namespace in
+ -- this record, then remove all declarations of it
+ IF xml_buf !~ E'<marc:' THEN
+ xml_buf := REGEXP_REPLACE(xml_buf,
+ ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g');
+ END IF;
+ ELSE
+ xml_buf := oils_xslt_process(xml_buf, xfrm.xslt)::XML;
+ END IF;
+
+ -- update top_el to reflect the change in xml_buf, which may
+ -- now be a different type of document (e.g. record -> mods)
+ top_el := REGEXP_REPLACE(xml_buf, E'^.*?<((?:\\S+:)?' ||
+ layout.holdings_element || ').*$', E'\\1');
+
+ IF axml IS NOT NULL THEN
+ xml_buf := REGEXP_REPLACE(xml_buf,
+ '</' || top_el || '>(.*?)$', axml || '</' || top_el || E'>\\1');
+ END IF;
+
+ IF hxml IS NOT NULL THEN
+ xml_buf := REGEXP_REPLACE(xml_buf,
+ '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1');
+ END IF;
+
+ IF ('mmr.unapi' = ANY (includes)) THEN
+ output := REGEXP_REPLACE(
+ xml_buf,
+ '</' || top_el || '>(.*?)',
+ XMLELEMENT(
+ name abbr,
+ XMLATTRIBUTES(
+ 'http://www.w3.org/1999/xhtml' AS xmlns,
+ 'unapi-id' AS class,
+ 'tag:open-ils.org:U2@mmr/' || obj_id || '/' || org AS title
+ )
+ )::TEXT || '</' || top_el || E'>\\1'
+ );
+ ELSE
+ output := xml_buf;
+ END IF;
+
+ -- remove ignorable whitesace
+ output := REGEXP_REPLACE(output::TEXT,E'>\\s+<','><','gs')::XML;
+ RETURN output;
+END;
+$F$ LANGUAGE PLPGSQL STABLE;
+
+DROP FUNCTION IF EXISTS unapi.mmr_holdings_xml (
+ mid BIGINT,
+ ouid INT,
+ org TEXT,
+ depth INT,
+ includes TEXT[],
+ slimit HSTORE,
+ soffset HSTORE,
+ include_xmlns BOOL,
+ pref_lib INT
+);
+
+CREATE OR REPLACE FUNCTION unapi.mmr_holdings_xml (
+ mid BIGINT,
+ ouid INT,
+ org TEXT,
+ depth INT DEFAULT NULL,
+ includes TEXT[] DEFAULT NULL::TEXT[],
+ slimit HSTORE DEFAULT NULL,
+ soffset HSTORE DEFAULT NULL,
+ include_xmlns BOOL DEFAULT TRUE,
+ pref_lib INT DEFAULT NULL,
+ library_group INT DEFAULT NULL
+)
+RETURNS XML AS $F$
+ SELECT XMLELEMENT(
+ name holdings,
+ XMLATTRIBUTES(
+ CASE WHEN $8 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+ CASE WHEN ('mmr' = ANY ($5)) THEN 'tag:open-ils.org:U2@mmr/' || $1 || '/' || $3 ELSE NULL END AS id,
+ (SELECT metarecord_has_holdable_copy FROM asset.metarecord_has_holdable_copy($1)) AS has_holdable
+ ),
+ XMLELEMENT(
+ name counts,
+ (SELECT XMLAGG(XMLELEMENT::XML) FROM (
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('public' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.opac_ou_metarecord_copy_count($2, $1)
+ UNION
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('staff' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.staff_ou_metarecord_copy_count($2, $1)
+ UNION
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('pref_lib' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.opac_ou_metarecord_copy_count($9, $1)
+ UNION
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('public' as type, depth, library_group, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.opac_lasso_metarecord_copy_count_sum(library_group, $1)
+ UNION
+ SELECT XMLELEMENT(
+ name count,
+ XMLATTRIBUTES('staff' as type, depth, library_group, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+ )::text
+ FROM asset.staff_lasso_metarecord_copy_count_sum(library_group, $1)
+ ORDER BY 1
+ )x)
+ ),
+ -- XXX monograph_parts and foreign_copies are skipped in MRs ... put them back some day?
+ XMLELEMENT(
+ name volumes,
+ (SELECT XMLAGG(acn ORDER BY rank, name, label_sortkey) FROM (
+ -- Physical copies
+ SELECT unapi.acn(y.id,'xml','volume',array_remove( array_remove($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE), y.rank, name, label_sortkey
+ FROM evergreen.ranked_volumes((SELECT ARRAY_AGG(source) FROM metabib.metarecord_source_map WHERE metarecord = $1), $2, $4, $6, $7, $9, $5) AS y
+ UNION ALL
+ -- Located URIs
+ SELECT unapi.acn(uris.id,'xml','volume',array_remove( array_remove($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE), uris.rank, name, label_sortkey
+ FROM evergreen.located_uris((SELECT ARRAY_AGG(source) FROM metabib.metarecord_source_map WHERE metarecord = $1), $2, $9) AS uris
+ )x)
+ ),
+ CASE WHEN ('ssub' = ANY ($5)) THEN
+ XMLELEMENT(
+ name subscriptions,
+ (SELECT XMLAGG(ssub) FROM (
+ SELECT unapi.ssub(id,'xml','subscription','{}'::TEXT[], $3, $4, $6, $7, FALSE)
+ FROM serial.subscription
+ WHERE record_entry IN (SELECT source FROM metabib.metarecord_source_map WHERE metarecord = $1)
+ )x)
+ )
+ ELSE NULL END
+ );
+$F$ LANGUAGE SQL STABLE;
+
+
+SELECT evergreen.upgrade_deps_block_check('1465', :eg_version);
+
+ALTER TABLE config.ui_staff_portal_page_entry
+ADD COLUMN url_newtab boolean;
+
+
+SELECT evergreen.upgrade_deps_block_check('1466', :eg_version);
+
+INSERT into config.workstation_setting_type
+ (name, grp, label, description, datatype)
+VALUES (
+ 'ui.staff.disable_links_newtabs',
+ 'gui',
+ oils_i18n_gettext('ui.staff.disable_links_newtabs',
+ 'Staff Client: no new tabs',
+ 'coust', 'label'),
+ oils_i18n_gettext('ui.staff.disable_links_newtabs',
+ 'Prevents links in the staff interface from opening in new tabs or windows.',
+ 'coust', 'description'),
+ 'bool'
+);
+
+
+SELECT evergreen.upgrade_deps_block_check('1467', :eg_version);
+
+CREATE TABLE action.eresource_link_click (
+ id BIGSERIAL PRIMARY KEY,
+ clicked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ url TEXT,
+ record BIGINT NOT NULL REFERENCES biblio.record_entry (id)
+);
+
+CREATE TABLE action.eresource_link_click_course (
+ id SERIAL PRIMARY KEY,
+ click BIGINT NOT NULL REFERENCES action.eresource_link_click (id) ON DELETE CASCADE,
+ course INT REFERENCES asset.course_module_course (id) ON UPDATE CASCADE ON DELETE SET NULL,
+ course_name TEXT NOT NULL,
+ course_number TEXT NOT NULL
+);
+
+INSERT INTO config.global_flag (name, label, enabled)
+ VALUES (
+ 'opac.eresources.link_click_tracking',
+ oils_i18n_gettext('opac.eresources.link_click_tracking',
+ 'Track clicks on eresources links. Before enabling this global flag, be sure that you are monitoring disk space on your database server and have a cron job set up to delete click records after the desired retention interval.',
+ 'cgf', 'label'),
+ FALSE
+ );
+
+CREATE FUNCTION action.delete_old_eresource_link_clicks(days integer)
+ RETURNS VOID AS
+ 'DELETE FROM action.eresource_link_click
+ WHERE clicked_at < current_timestamp
+ - ($1::text || '' days'')::interval'
+ LANGUAGE SQL
+ VOLATILE;
+
+
+
+SELECT evergreen.upgrade_deps_block_check('1468', :eg_version);
+
+-- Necessary pre-seed data
+
+
+INSERT INTO actor.passwd_type (code, name, login, crypt_algo, iter_count)
+ VALUES ('api', 'OpenAPI Integration Password', TRUE, 'bf', 10)
+ON CONFLICT DO NOTHING;
+
+-- Move top-level perms "down" ...
+INSERT INTO permission.grp_perm_map (grp,perm,depth,grantable)
+ SELECT DISTINCT g.id, p.perm, p.depth, p.grantable
+ FROM permission.grp_perm_map p,
+ permission.grp_tree g
+ WHERE g.parent = 1 AND p.grp = 1;
+
+-- ... then remove the User version ...
+DELETE FROM permission.grp_perm_map WHERE grp = 1;
+
+-- ... and add a new branch to the group tree for API perms
+INSERT INTO permission.grp_tree (name, parent, description, application_perm)
+ VALUES ('API Integrator', 1, 'API Integration Accounts', 'group_application.api_integrator');
+
+INSERT INTO permission.grp_tree (name, parent, description, application_perm) SELECT 'Patron API', id, 'Patron API', 'group_application.api_integrator' FROM permission.grp_tree WHERE name = 'API Integrator';
+INSERT INTO permission.grp_tree (name, parent, description, application_perm) SELECT 'Org Unit API', id, 'Org Unit API', 'group_application.api_integrator' FROM permission.grp_tree WHERE name = 'API Integrator';
+INSERT INTO permission.grp_tree (name, parent, description, application_perm) SELECT 'Bib Record API', id, 'Bib Record API', 'group_application.api_integrator' FROM permission.grp_tree WHERE name = 'API Integrator';
+INSERT INTO permission.grp_tree (name, parent, description, application_perm) SELECT 'Item Record API', id, 'Item Record API', 'group_application.api_integrator' FROM permission.grp_tree WHERE name = 'API Integrator';
+INSERT INTO permission.grp_tree (name, parent, description, application_perm) SELECT 'Holds API', id, 'Holds API', 'group_application.api_integrator' FROM permission.grp_tree WHERE name = 'API Integrator';
+INSERT INTO permission.grp_tree (name, parent, description, application_perm) SELECT 'Debt Collection API', id, 'Debt Collection API', 'group_application.api_integrator' FROM permission.grp_tree WHERE name = 'API Integrator';
+INSERT INTO permission.grp_tree (name, parent, description, application_perm) SELECT 'Course Reserves API', id, 'Course Reserves API', 'group_application.api_integrator' FROM permission.grp_tree WHERE name = 'API Integrator';
+
+INSERT INTO permission.perm_list (code) VALUES
+ ('group_application.api_integrator'),
+ ('API_LOGIN'),
+ ('REST.api'),
+ ('REST.api.patrons'),
+ ('REST.api.orgs'),
+ ('REST.api.bibs'),
+ ('REST.api.items'),
+ ('REST.api.holds'),
+ ('REST.api.collections'),
+ ('REST.api.courses')
+ --- ... etc
+ON CONFLICT DO NOTHING;
+
+INSERT INTO permission.grp_perm_map (grp,perm,depth)
+ SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='API Integrator' AND p.code IN ('API_LOGIN','REST.api')
+ UNION
+ SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Patron API' AND p.code = 'REST.api.patrons'
+ UNION
+ SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Org Unit API' AND p.code = 'REST.api.orgs'
+ UNION
+ SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Bib Record API' AND p.code = 'REST.api.bibs'
+ UNION
+ SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Item Record API' AND p.code = 'REST.api.items'
+ UNION
+ SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Holds API' AND p.code = 'REST.api.holds'
+ UNION
+ SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Debt Collection API' AND p.code = 'REST.api.collections'
+ UNION
+ SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Course Reserves API' AND p.code = 'REST.api.courses'
+ON CONFLICT DO NOTHING;
+
+DROP SCHEMA IF EXISTS openapi CASCADE;
+CREATE SCHEMA IF NOT EXISTS openapi;
+
+CREATE TABLE IF NOT EXISTS openapi.integrator (
+ id INT PRIMARY KEY REFERENCES actor.usr (id),
+ enabled BOOL NOT NULL DEFAULT TRUE
+);
+
+CREATE TABLE IF NOT EXISTS openapi.json_schema_datatype (
+ name TEXT PRIMARY KEY,
+ label TEXT NOT NULL UNIQUE,
+ description TEXT
+);
+INSERT INTO openapi.json_schema_datatype (name,label) VALUES
+ ('boolean','Boolean'),
+ ('string','String'),
+ ('integer','Integer'),
+ ('number','Number'),
+ ('array','Array'),
+ ('object','Object')
+ON CONFLICT DO NOTHING;
+
+CREATE TABLE IF NOT EXISTS openapi.json_schema_format (
+ name TEXT PRIMARY KEY,
+ label TEXT NOT NULL UNIQUE,
+ description TEXT
+);
+INSERT INTO openapi.json_schema_format (name,label) VALUES
+ ('date-time','Timestamp'),
+ ('date','Date'),
+ ('time','Time'),
+ ('interval','Interval'),
+ ('email','Email Address'),
+ ('uri','URI'),
+ ('identifier','Opaque Identifier'),
+ ('money','Money'),
+ ('float','Floating Point Number'),
+ ('int64','Large Integer'),
+ ('password','Password')
+ON CONFLICT DO NOTHING;
+
+CREATE TABLE IF NOT EXISTS openapi.rate_limit_definition (
+ id SERIAL PRIMARY KEY,
+ name TEXT UNIQUE NOT NULL, -- i18n
+ limit_interval INTERVAL NOT NULL,
+ limit_count INT NOT NULL
+);
+SELECT SETVAL('openapi.rate_limit_definition_id_seq'::TEXT, 100);
+INSERT INTO openapi.rate_limit_definition (id, name, limit_interval, limit_count) VALUES
+ (1, 'Once per second', '1 second', 1),
+ (2, 'Ten per minute', '1 minute', 10),
+ (3, 'One hunderd per hour', '1 hour', 100),
+ (4, 'One thousand per hour', '1 hour', 1000),
+ (5, 'One thousand per 24 hour period', '24 hours', 1000),
+ (6, 'Ten thousand per 24 hour period', '24 hours', 10000),
+ (7, 'Unlimited', '1 second', 1000000),
+ (8, 'One hundred per second', '1 second', 100)
+ON CONFLICT DO NOTHING;
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint (
+ operation_id TEXT PRIMARY KEY,
+ path TEXT NOT NULL,
+ http_method TEXT NOT NULL CHECK (http_method IN ('get','put','post','delete','patch')),
+ security TEXT NOT NULL DEFAULT 'bearerAuth' CHECK (security IN ('bearerAuth','basicAuth','cookieAuth','paramAuth')),
+ summary TEXT NOT NULL,
+ method_source TEXT NOT NULL, -- perl module or opensrf application, tested by regex and assumes opensrf app name contains a "."
+ method_name TEXT NOT NULL,
+ method_params TEXT, -- eg, 'eg_auth_token hold' or 'eg_auth_token eg_user_id circ req.json'
+ active BOOL NOT NULL DEFAULT TRUE,
+ rate_limit INT REFERENCES openapi.rate_limit_definition (id),
+ CONSTRAINT path_and_method_once UNIQUE (path, http_method)
+);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_param (
+ id SERIAL PRIMARY KEY,
+ endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ name TEXT NOT NULL CHECK (name ~ '^\w+$'),
+ required BOOL NOT NULL DEFAULT FALSE,
+ in_part TEXT NOT NULL DEFAULT 'query' CHECK (in_part IN ('path','query','header','cookie')),
+ fm_type TEXT,
+ schema_type TEXT REFERENCES openapi.json_schema_datatype (name),
+ schema_format TEXT REFERENCES openapi.json_schema_format (name),
+ array_items TEXT REFERENCES openapi.json_schema_datatype (name),
+ default_value TEXT,
+ CONSTRAINT endpoint_and_name_once UNIQUE (endpoint, name),
+ CONSTRAINT format_requires_type CHECK (schema_format IS NULL OR schema_type IS NOT NULL),
+ CONSTRAINT array_items_requires_array_type CHECK (array_items IS NULL OR schema_type = 'array')
+);
+CREATE TABLE IF NOT EXISTS openapi.endpoint_response (
+ id SERIAL PRIMARY KEY,
+ endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ validate BOOL NOT NULL DEFAULT TRUE,
+ status INT NOT NULL DEFAULT 200,
+ content_type TEXT NOT NULL DEFAULT 'application/json',
+ description TEXT NOT NULL DEFAULT 'Success',
+ fm_type TEXT,
+ schema_type TEXT REFERENCES openapi.json_schema_datatype (name),
+ schema_format TEXT REFERENCES openapi.json_schema_format (name),
+ array_items TEXT REFERENCES openapi.json_schema_datatype (name),
+ CONSTRAINT endpoint_status_content_type_once UNIQUE (endpoint, status, content_type)
+);
+
+CREATE TABLE IF NOT EXISTS openapi.perm_set (
+ id SERIAL PRIMARY KEY,
+ name TEXT NOT NULL
+); -- push sequence value
+SELECT SETVAL('openapi.perm_set_id_seq'::TEXT, 1001);
+INSERT INTO openapi.perm_set (id, name) VALUES
+ (1,'Self - API only'),
+ (2,'Patrons - API only'),
+ (3,'Orgs - API only'),
+ (4,'Bibs - API only'),
+ (5,'Items - API only'),
+ (6,'Holds - API only'),
+ (7,'Collections - API only'),
+ (8,'Courses - API only'),
+
+ (101,'Self - standard permissions'),
+ (102,'Patrons - standard permissions'),
+ (103,'Orgs - standard permissions'),
+ (104,'Bibs - standard permissions'),
+ (105,'Items - standard permissions'),
+ (106,'Holds - standard permissions'),
+ (107,'Collections - standard permissions'),
+ (108,'Courses - standard permissions')
+ON CONFLICT DO NOTHING;
+
+CREATE TABLE IF NOT EXISTS openapi.perm_set_perm_map (
+ id SERIAL PRIMARY KEY,
+ perm_set INT NOT NULL REFERENCES openapi.perm_set (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ perm INT NOT NULL REFERENCES permission.perm_list (id) ON UPDATE CASCADE ON DELETE CASCADE
+);
+INSERT INTO openapi.perm_set_perm_map (perm_set, perm)
+ SELECT 1, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api')
+ UNION
+ SELECT 2, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.patrons')
+ UNION
+ SELECT 3, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.orgs')
+ UNION
+ SELECT 4, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.bibs')
+ UNION
+ SELECT 5, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.items')
+ UNION
+ SELECT 6, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.holds')
+ UNION
+ SELECT 7, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.collections')
+ UNION
+ SELECT 8, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.cources')
+ UNION
+ -- ...
+ SELECT 101, id FROM permission.perm_list WHERE code IN ('OPAC_LOGIN')
+ UNION
+ SELECT 102, id FROM permission.perm_list WHERE code IN ('STAFF_LOGIN','VIEW_USER')
+ UNION
+ SELECT 103, id FROM permission.perm_list WHERE code IN ('OPAC_LOGIN')
+ UNION
+ SELECT 104, id FROM permission.perm_list WHERE code IN ('OPAC_LOGIN')
+ UNION
+ SELECT 105, id FROM permission.perm_list WHERE code IN ('OPAC_LOGIN')
+ UNION
+ SELECT 106, id FROM permission.perm_list WHERE code IN ('STAFF_LOGIN','VIEW_USER')
+ UNION
+ SELECT 107, id FROM permission.perm_list WHERE code IN ('STAFF_LOGIN','VIEW_USER')
+ UNION
+ SELECT 108, id FROM permission.perm_list WHERE code IN ('STAFF_LOGIN')
+
+ON CONFLICT DO NOTHING;
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_perm_set_map (
+ id SERIAL PRIMARY KEY,
+ endpoint TEXT NOT NULL REFERENCES openapi.endpoint(operation_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ perm_set INT NOT NULL REFERENCES openapi.perm_set (id) ON UPDATE CASCADE ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_set (
+ name TEXT PRIMARY KEY,
+ description TEXT NOT NULL,
+ active BOOL NOT NULL DEFAULT TRUE,
+ rate_limit INT REFERENCES openapi.rate_limit_definition (id)
+);
+INSERT INTO openapi.endpoint_set (name, description) VALUES
+ ('self', 'Methods for retrieving and manipulating your own user account information'),
+ ('orgs', 'Methods for retrieving and manipulating organizational unit information'),
+ ('patrons', 'Methods for retrieving and manipulating patron information'),
+ ('holds', 'Methods for accessing and manipulating hold data'),
+ ('collections', 'Methods for accessing and manipulating patron debt collections data'),
+ ('bibs', 'Methods for accessing and manipulating bibliographic records and related data'),
+ ('items', 'Methods for accessing and manipulating barcoded item records'),
+ ('courses', 'Methods for accessing and manipulating course reserve data')
+ON CONFLICT DO NOTHING;
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_user_rate_limit_map (
+ id SERIAL PRIMARY KEY,
+ accessor INT NOT NULL REFERENCES actor.usr (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ rate_limit INT NOT NULL REFERENCES openapi.rate_limit_definition (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT endpoint_accessor_once UNIQUE (accessor, endpoint)
+);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_set_user_rate_limit_map (
+ id SERIAL PRIMARY KEY,
+ accessor INT NOT NULL REFERENCES actor.usr (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ endpoint_set TEXT NOT NULL REFERENCES openapi.endpoint_set (name) ON UPDATE CASCADE ON DELETE CASCADE,
+ rate_limit INT NOT NULL REFERENCES openapi.rate_limit_definition (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT endpoint_set_accessor_once UNIQUE (accessor, endpoint_set)
+);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_ip_rate_limit_map (
+ id SERIAL PRIMARY KEY,
+ ip_range INET NOT NULL,
+ endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ rate_limit INT NOT NULL REFERENCES openapi.rate_limit_definition (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT endpoint_ip_range_once UNIQUE (ip_range, endpoint)
+);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_set_ip_rate_limit_map (
+ id SERIAL PRIMARY KEY,
+ ip_range INET NOT NULL,
+ endpoint_set TEXT NOT NULL REFERENCES openapi.endpoint_set (name) ON UPDATE CASCADE ON DELETE CASCADE,
+ rate_limit INT NOT NULL REFERENCES openapi.rate_limit_definition (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT endpoint_set_ip_range_once UNIQUE (ip_range, endpoint_set)
+);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_set_endpoint_map (
+ id SERIAL PRIMARY KEY,
+ endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ endpoint_set TEXT NOT NULL REFERENCES openapi.endpoint_set (name) ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT endpoint_set_endpoint_once UNIQUE (endpoint_set, endpoint)
+);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_perm_map (
+ id SERIAL PRIMARY KEY,
+ endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ perm INT NOT NULL REFERENCES permission.perm_list (id) ON UPDATE CASCADE ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_set_perm_set_map (
+ id SERIAL PRIMARY KEY,
+ endpoint_set TEXT NOT NULL REFERENCES openapi.endpoint_set (name) ON UPDATE CASCADE ON DELETE CASCADE,
+ perm_set INT NOT NULL REFERENCES openapi.perm_set (id) ON UPDATE CASCADE ON DELETE CASCADE
+);
+INSERT INTO openapi.endpoint_set_perm_set_map (endpoint_set, perm_set) VALUES
+ ('self', 1), ('self', 101),
+ ('patrons', 2), ('patrons', 102),
+ ('orgs', 3), ('orgs', 103),
+ ('bibs', 4), ('bibs', 104),
+ ('items', 5), ('items', 105),
+ ('holds', 6), ('holds', 106),
+ ('collections', 7), ('collections', 107),
+ ('courses', 8), ('courses', 108)
+ON CONFLICT DO NOTHING;
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_set_perm_map (
+ id SERIAL PRIMARY KEY,
+ endpoint_set TEXT NOT NULL REFERENCES openapi.endpoint_set (name) ON UPDATE CASCADE ON DELETE CASCADE,
+ perm INT NOT NULL REFERENCES permission.perm_list (id) ON UPDATE CASCADE ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS openapi.authen_attempt_log (
+ request_id TEXT PRIMARY KEY,
+ attempt_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ ip_addr INET,
+ cred_user TEXT,
+ token TEXT
+);
+CREATE INDEX authen_cred_user_attempt_time_idx ON openapi.authen_attempt_log (attempt_time, cred_user);
+CREATE INDEX authen_ip_addr_attempt_time_idx ON openapi.authen_attempt_log (attempt_time, ip_addr);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_access_attempt_log (
+ request_id TEXT PRIMARY KEY,
+ attempt_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ allowed BOOL NOT NULL,
+ ip_addr INET,
+ accessor INT REFERENCES actor.usr (id) ON UPDATE CASCADE ON DELETE CASCADE,
+ token TEXT
+);
+CREATE INDEX access_accessor_attempt_time_idx ON openapi.endpoint_access_attempt_log (accessor, attempt_time);
+
+CREATE TABLE IF NOT EXISTS openapi.endpoint_dispatch_log (
+ request_id TEXT PRIMARY KEY,
+ complete_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ error BOOL NOT NULL
+);
+
+CREATE OR REPLACE FUNCTION actor.verify_passwd(pw_usr integer, pw_type text, test_passwd text) RETURNS boolean AS $f$
+DECLARE
+ pw_salt TEXT;
+ api_enabled BOOL;
+BEGIN
+ /* Returns TRUE if the password provided matches the in-db password.
+ * If the password type is salted, we compare the output of CRYPT().
+ * NOTE: test_passwd is MD5(salt || MD5(password)) for legacy
+ * 'main' passwords.
+ *
+ * Password type 'api' requires that the user be enabled as an
+ * integrator in the openapi.integrator table.
+ */
+
+ IF pw_type = 'api' THEN
+ SELECT enabled INTO api_enabled
+ FROM openapi.integrator
+ WHERE id = pw_usr;
+
+ IF NOT FOUND OR api_enabled IS FALSE THEN
+ -- API integrator account not registered
+ RETURN FALSE;
+ END IF;
+ END IF;
+
+ SELECT INTO pw_salt salt FROM actor.passwd
+ WHERE usr = pw_usr AND passwd_type = pw_type;
+
+ IF NOT FOUND THEN
+ -- no such password
+ RETURN FALSE;
+ END IF;
+
+ IF pw_salt IS NULL THEN
+ -- Password is unsalted, compare the un-CRYPT'ed values.
+ RETURN EXISTS (
+ SELECT TRUE FROM actor.passwd WHERE
+ usr = pw_usr AND
+ passwd_type = pw_type AND
+ passwd = test_passwd
+ );
+ END IF;
+
+ RETURN EXISTS (
+ SELECT TRUE FROM actor.passwd WHERE
+ usr = pw_usr AND
+ passwd_type = pw_type AND
+ passwd = CRYPT(test_passwd, pw_salt)
+ );
+END;
+$f$ STRICT LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION openapi.find_default_endpoint_rate_limit (target_endpoint TEXT) RETURNS openapi.rate_limit_definition AS $f$
+DECLARE
+ def_rl openapi.rate_limit_definition%ROWTYPE;
+BEGIN
+ -- Default rate limits can be applied at the endpoint or endpoint_set level;
+ -- endpoint overrides endpoint_set, and we choose the most restrictive from
+ -- the set if we have to look there.
+ SELECT d.* INTO def_rl
+ FROM openapi.rate_limit_definition d
+ JOIN openapi.endpoint e ON (e.rate_limit = d.id)
+ WHERE e.operation_id = target_endpoint;
+
+ IF NOT FOUND THEN
+ SELECT d.* INTO def_rl
+ FROM openapi.rate_limit_definition d
+ JOIN openapi.endpoint_set es ON (es.rate_limit = d.id)
+ JOIN openapi.endpoint_set_endpoint_map m ON (es.name = m.endpoint_set AND m.endpoint = target_endpoint)
+ -- This ORDER BY calculates the avg time between requests the user would have to wait to perfectly
+ -- avoid rate limiting. So, a bigger wait means it's more restrictive. We take the most restrictive
+ -- set-applied one.
+ ORDER BY EXTRACT(EPOCH FROM d.limit_interval) / d.limit_count::NUMERIC DESC
+ LIMIT 1;
+ END IF;
+
+ -- If there's no default for the endpoint or set, we provide 1/sec.
+ IF NOT FOUND THEN
+ def_rl.limit_interval := '1 second'::INTERVAL;
+ def_rl.limit_count := 1;
+ END IF;
+
+ RETURN def_rl;
+END;
+$f$ STABLE LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION openapi.find_user_endpoint_rate_limit (target_endpoint TEXT, accessing_usr INT) RETURNS openapi.rate_limit_definition AS $f$
+DECLARE
+ def_u_rl openapi.rate_limit_definition%ROWTYPE;
+BEGIN
+ SELECT d.* INTO def_u_rl
+ FROM openapi.rate_limit_definition d
+ JOIN openapi.endpoint_user_rate_limit_map e ON (e.rate_limit = d.id)
+ WHERE e.endpoint = target_endpoint
+ AND e.accessor = accessing_usr;
+
+ IF NOT FOUND THEN
+ SELECT d.* INTO def_u_rl
+ FROM openapi.rate_limit_definition d
+ JOIN openapi.endpoint_set_user_rate_limit_map e ON (e.rate_limit = d.id AND e.accessor = accessing_usr)
+ JOIN openapi.endpoint_set_endpoint_map m ON (e.endpoint_set = m.endpoint_set AND m.endpoint = target_endpoint)
+ ORDER BY EXTRACT(EPOCH FROM d.limit_interval) / d.limit_count::NUMERIC DESC
+ LIMIT 1;
+ END IF;
+
+ RETURN def_u_rl;
+END;
+$f$ STABLE LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION openapi.find_ip_addr_endpoint_rate_limit (target_endpoint TEXT, from_ip_addr INET) RETURNS openapi.rate_limit_definition AS $f$
+DECLARE
+ def_i_rl openapi.rate_limit_definition%ROWTYPE;
+BEGIN
+ SELECT d.* INTO def_i_rl
+ FROM openapi.rate_limit_definition d
+ JOIN openapi.endpoint_ip_rate_limit_map e ON (e.rate_limit = d.id)
+ WHERE e.endpoint = target_endpoint
+ AND e.ip_range && from_ip_addr
+ -- For IPs, we order first by the size of the ranges that we
+ -- matched (mask length), most specific (smallest block of IPs)
+ -- first, then by the restrictiveness of the limit, more restrictive first.
+ ORDER BY MASKLEN(e.ip_range) DESC, EXTRACT(EPOCH FROM d.limit_interval) / d.limit_count::NUMERIC DESC
+ LIMIT 1;
+
+ IF NOT FOUND THEN
+ SELECT d.* INTO def_i_rl
+ FROM openapi.rate_limit_definition d
+ JOIN openapi.endpoint_set_ip_rate_limit_map e ON (e.rate_limit = d.id AND ip_range && from_ip_addr)
+ JOIN openapi.endpoint_set_endpoint_map m ON (e.endpoint_set = m.endpoint_set AND m.endpoint = target_endpoint)
+ ORDER BY MASKLEN(e.ip_range) DESC, EXTRACT(EPOCH FROM d.limit_interval) / d.limit_count::NUMERIC DESC
+ LIMIT 1;
+ END IF;
+
+ RETURN def_i_rl;
+END;
+$f$ STABLE LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION openapi.check_generic_endpoint_rate_limit (target_endpoint TEXT, accessing_usr INT DEFAULT NULL, from_ip_addr INET DEFAULT NULL) RETURNS INT AS $f$
+DECLARE
+ def_rl openapi.rate_limit_definition%ROWTYPE;
+ def_u_rl openapi.rate_limit_definition%ROWTYPE;
+ def_i_rl openapi.rate_limit_definition%ROWTYPE;
+ u_wait INT;
+ i_wait INT;
+BEGIN
+ def_rl := openapi.find_default_endpoint_rate_limit(target_endpoint);
+
+ IF accessing_usr IS NOT NULL THEN
+ def_u_rl := openapi.find_user_endpoint_rate_limit(target_endpoint, accessing_usr);
+ END IF;
+
+ IF from_ip_addr IS NOT NULL THEN
+ def_i_rl := openapi.find_ip_addr_endpoint_rate_limit(target_endpoint, from_ip_addr);
+ END IF;
+
+ -- Now we test the user-based and IP-based limits in their focused way...
+ IF def_u_rl.id IS NOT NULL THEN
+ SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_u_rl.limit_interval) - NOW())) INTO u_wait
+ FROM (SELECT l.attempt_time,
+ COUNT(*) OVER (PARTITION BY l.accessor ORDER BY l.attempt_time DESC) AS running_count
+ FROM openapi.endpoint_access_attempt_log l
+ WHERE l.endpoint = target_endpoint
+ AND l.accessor = accessing_usr
+ AND l.attempt_time > NOW() - def_u_rl.limit_interval
+ ) x
+ WHERE running_count = def_u_rl.limit_count;
+ END IF;
+
+ IF def_i_rl.id IS NOT NULL THEN
+ SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_u_rl.limit_interval) - NOW())) INTO i_wait
+ FROM (SELECT l.attempt_time,
+ COUNT(*) OVER (PARTITION BY l.ip_addr ORDER BY l.attempt_time DESC) AS running_count
+ FROM openapi.endpoint_access_attempt_log l
+ WHERE l.endpoint = target_endpoint
+ AND l.ip_addr = from_ip_addr
+ AND l.attempt_time > NOW() - def_u_rl.limit_interval
+ ) x
+ WHERE running_count = def_u_rl.limit_count;
+ END IF;
+
+ -- If there are no user-specific or IP-based limit
+ -- overrides; check endpoint-wide limits for user,
+ -- then IP, and if we were passed neither, then limit
+ -- endpoint access for all users. Better to lock it
+ -- all down than to set the servers on fire.
+ IF COALESCE(u_wait, i_wait) IS NULL AND COALESCE(def_i_rl.id, def_u_rl.id) IS NULL THEN
+ IF accessing_usr IS NOT NULL THEN
+ SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_rl.limit_interval) - NOW())) INTO u_wait
+ FROM (SELECT l.attempt_time,
+ COUNT(*) OVER (PARTITION BY l.accessor ORDER BY l.attempt_time DESC) AS running_count
+ FROM openapi.endpoint_access_attempt_log l
+ WHERE l.endpoint = target_endpoint
+ AND l.accessor = accessing_usr
+ AND l.attempt_time > NOW() - def_rl.limit_interval
+ ) x
+ WHERE running_count = def_rl.limit_count;
+ ELSIF from_ip_addr IS NOT NULL THEN
+ SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_rl.limit_interval) - NOW())) INTO i_wait
+ FROM (SELECT l.attempt_time,
+ COUNT(*) OVER (PARTITION BY l.ip_addr ORDER BY l.attempt_time DESC) AS running_count
+ FROM openapi.endpoint_access_attempt_log l
+ WHERE l.endpoint = target_endpoint
+ AND l.ip_addr = from_ip_addr
+ AND l.attempt_time > NOW() - def_rl.limit_interval
+ ) x
+ WHERE running_count = def_rl.limit_count;
+ ELSE -- we have no user and no IP, global per-endpoint rate limit?
+ SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_rl.limit_interval) - NOW())) INTO i_wait
+ FROM (SELECT l.attempt_time,
+ COUNT(*) OVER (PARTITION BY l.endpoint ORDER BY l.attempt_time DESC) AS running_count
+ FROM openapi.endpoint_access_attempt_log l
+ WHERE l.endpoint = target_endpoint
+ AND l.attempt_time > NOW() - def_rl.limit_interval
+ ) x
+ WHERE running_count = def_rl.limit_count;
+ END IF;
+ END IF;
+
+ -- Send back the largest required wait time, or NULL for no restriction
+ u_wait := GREATEST(u_wait,i_wait);
+ IF u_wait > 0 THEN
+ RETURN u_wait;
+ END IF;
+
+ RETURN NULL;
+END;
+$f$ STABLE LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION openapi.check_auth_endpoint_rate_limit (accessing_usr TEXT DEFAULT NULL, from_ip_addr INET DEFAULT NULL) RETURNS INT AS $f$
+DECLARE
+ def_rl openapi.rate_limit_definition%ROWTYPE;
+ def_u_rl openapi.rate_limit_definition%ROWTYPE;
+ def_i_rl openapi.rate_limit_definition%ROWTYPE;
+ u_wait INT;
+ i_wait INT;
+BEGIN
+ def_rl := openapi.find_default_endpoint_rate_limit('authenticateUser');
+
+ IF accessing_usr IS NOT NULL THEN
+ SELECT (openapi.find_user_endpoint_rate_limit('authenticateUser', u.id)).* INTO def_u_rl
+ FROM actor.usr u
+ WHERE u.usrname = accessing_usr;
+ END IF;
+
+ IF from_ip_addr IS NOT NULL THEN
+ def_i_rl := openapi.find_ip_addr_endpoint_rate_limit('authenticateUser', from_ip_addr);
+ END IF;
+
+ -- Now we test the user-based and IP-based limits in their focused way...
+ IF def_u_rl.id IS NOT NULL THEN
+ SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_u_rl.limit_interval) - NOW())) INTO u_wait
+ FROM (SELECT l.attempt_time,
+ COUNT(*) OVER (PARTITION BY l.cred_user ORDER BY l.attempt_time DESC) AS running_count
+ FROM openapi.authen_attempt_log l
+ WHERE l.cred_user = accessing_usr
+ AND l.attempt_time > NOW() - def_u_rl.limit_interval
+ ) x
+ WHERE running_count = def_u_rl.limit_count;
+ END IF;
+
+ IF def_i_rl.id IS NOT NULL THEN
+ SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_u_rl.limit_interval) - NOW())) INTO i_wait
+ FROM (SELECT l.attempt_time,
+ COUNT(*) OVER (PARTITION BY l.ip_addr ORDER BY l.attempt_time DESC) AS running_count
+ FROM openapi.authen_attempt_log l
+ WHERE l.ip_addr = from_ip_addr
+ AND l.attempt_time > NOW() - def_u_rl.limit_interval
+ ) x
+ WHERE running_count = def_u_rl.limit_count;
+ END IF;
+
+ -- If there are no user-specific or IP-based limit
+ -- overrides; check endpoint-wide limits for user,
+ -- then IP, and if we were passed neither, then limit
+ -- endpoint access for all users. Better to lock it
+ -- all down than to set the servers on fire.
+ IF COALESCE(u_wait, i_wait) IS NULL AND COALESCE(def_i_rl.id, def_u_rl.id) IS NULL THEN
+ IF accessing_usr IS NOT NULL THEN
+ SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_rl.limit_interval) - NOW())) INTO u_wait
+ FROM (SELECT l.attempt_time,
+ COUNT(*) OVER (PARTITION BY l.cred_user ORDER BY l.attempt_time DESC) AS running_count
+ FROM openapi.authen_attempt_log l
+ WHERE l.cred_user = accessing_usr
+ AND l.attempt_time > NOW() - def_rl.limit_interval
+ ) x
+ WHERE running_count = def_rl.limit_count;
+ ELSIF from_ip_addr IS NOT NULL THEN
+ SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_rl.limit_interval) - NOW())) INTO i_wait
+ FROM (SELECT l.attempt_time,
+ COUNT(*) OVER (PARTITION BY l.ip_addr ORDER BY l.attempt_time DESC) AS running_count
+ FROM openapi.authen_attempt_log l
+ WHERE l.ip_addr = from_ip_addr
+ AND l.attempt_time > NOW() - def_rl.limit_interval
+ ) x
+ WHERE running_count = def_rl.limit_count;
+ ELSE -- we have no user and no IP, global auth attempt rate limit?
+ SELECT CEIL(EXTRACT(EPOCH FROM (l.attempt_time + def_rl.limit_interval) - NOW())) INTO u_wait
+ FROM openapi.authen_attempt_log l
+ WHERE l.attempt_time > NOW() - def_rl.limit_interval
+ ORDER BY l.attempt_time DESC
+ LIMIT 1 OFFSET def_rl.limit_count;
+ END IF;
+ END IF;
+
+ -- Send back the largest required wait time, or NULL for no restriction
+ u_wait := GREATEST(u_wait,i_wait);
+ IF u_wait > 0 THEN
+ RETURN u_wait;
+ END IF;
+
+ RETURN NULL;
+END;
+$f$ STABLE LANGUAGE PLPGSQL;
+
+--===================================== Seed data ==================================
+
+-- ===== authentication
+INSERT INTO openapi.endpoint (operation_id, path, security, http_method, summary, method_source, method_name, method_params, rate_limit) VALUES
+-- Builtin auth-related methods, give all users and IPs 100/s of each
+ ('authenticateUser', '/self/auth', 'basicAuth', 'get', 'Authenticate API user', 'OpenILS::OpenAPI::Controller', 'authenticateUser', 'param.u param.p param.t', 8),
+ ('logoutUser', '/self/auth', 'bearerAuth', 'delete', 'Logout API user', 'open-ils.auth', 'open-ils.auth.session.delete', 'eg_auth_token', 8)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value) VALUES
+ ('authenticateUser','u','query','string',NULL,NULL),
+ ('authenticateUser','p','query','string','password',NULL),
+ ('authenticateUser','t','query','string',NULL,'api')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('authenticateUser','object') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,content_type) VALUES ('authenticateUser','text/plain') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('logoutUser','object') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,content_type) VALUES ('logoutUser','text/plain') ON CONFLICT DO NOTHING;
+
+-- ===== self-service
+-- get/update me
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrieveSelfProfile', '/self/me', 'get', 'Return patron/user record for logged in user', 'OpenILS::OpenAPI::Controller::patron', 'deliver_user', 'eg_auth_token eg_user_id'),
+ ('selfUpdateParts', '/self/me', 'patch', 'Update portions of the logged in user''s record', 'OpenILS::OpenAPI::Controller::patron', 'update_user_parts', 'eg_auth_token req.json')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveSelfProfile','au') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('selfUpdateParts','object') ON CONFLICT DO NOTHING;
+
+-- get my standing penalties
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'selfActivePenalties',
+ '/self/standing_penalties',
+ 'get',
+ 'Produces patron-visible penalty details for the authorized account',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'standing_penalties',
+ 'eg_auth_token eg_user_id "1"'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('selfActivePenalties','array','object') ON CONFLICT DO NOTHING; -- array of fleshed ausp
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'selfPenalty',
+ '/self/standing_penalty/:penaltyid',
+ 'get',
+ 'Retrieve one penalty for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'standing_penalties',
+ 'eg_auth_token eg_user_id "1" param.penaltyid'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('selfPenalty','penaltyid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+
+
+
+-- manage my holds
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrieveSelfHolds', '/self/holds', 'get', 'Return unfilled holds for the authorized account', 'OpenILS::OpenAPI::Controller::hold', 'open_holds', 'eg_auth_token eg_user_id'),
+ ('requestSelfHold', '/self/holds', 'post', 'Request a hold for the authorized account', 'OpenILS::OpenAPI::Controller::hold', 'request_hold', 'eg_auth_token eg_user_id req.json'),
+ ('retrieveSelfHold', '/self/hold/:hold', 'get', 'Retrieve one hold for the logged in user', 'OpenILS::OpenAPI::Controller::hold', 'fetch_user_hold', 'eg_auth_token eg_user_id param.hold'),
+ ('updateSelfHold', '/self/hold/:hold', 'patch', 'Update one hold for the logged in user', 'OpenILS::OpenAPI::Controller::hold', 'update_user_hold', 'eg_auth_token eg_user_id param.hold req.json'),
+ ('cancelSelfHold', '/self/hold/:hold', 'delete', 'Cancel one hold for the logged in user', 'OpenILS::OpenAPI::Controller::hold', 'cancel_user_hold', 'eg_auth_token eg_user_id param.hold "6"')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES
+ ('retrieveSelfHold','hold','path','integer',TRUE),
+ ('updateSelfHold','hold','path','integer',TRUE),
+ ('cancelSelfHold','hold','path','integer',TRUE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveSelfHolds','array','object') ON CONFLICT DO NOTHING;
+
+
+-- general xact list
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrieveSelfXacts',
+ '/self/transactions/:state',
+ 'get',
+ 'Produces a list of transactions of the logged in user',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'transactions_by_state',
+ 'eg_auth_token eg_user_id state param.limit param.offset param.sort param.before param.after'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value,required) VALUES
+ ('retrieveSelfXacts','state','path','string',NULL,NULL,TRUE),
+ ('retrieveSelfXacts','limit','query','integer',NULL,NULL,FALSE),
+ ('retrieveSelfXacts','offset','query','integer',NULL,'0',FALSE),
+ ('retrieveSelfXacts','sort','query','string',NULL,'desc',FALSE),
+ ('retrieveSelfXacts','before','query','string','date-time',NULL,FALSE),
+ ('retrieveSelfXacts','after','query','string','date-time',NULL,FALSE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveSelfXacts','array','object') ON CONFLICT DO NOTHING;
+
+-- general xact detail
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrieveSelfXact',
+ '/self/transaction/:id',
+ 'get',
+ 'Details of one transaction for the logged in user',
+ 'open-ils.actor',
+ 'open-ils.actor.user.transaction.fleshed.retrieve',
+ 'eg_auth_token param.id'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveSelfXact','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrieveSelfCircs', '/self/checkouts', 'get', 'Open Circs for the logged in user', 'open-ils.circ', 'open-ils.circ.actor.user.checked_out.atomic', 'eg_auth_token eg_user_id'),
+ ('requestSelfCirc', '/self/checkouts', 'post', 'Attempt a circulation for the logged in user', 'OpenILS::OpenAPI::Controller::patron', 'checkout_item', 'eg_auth_token eg_user_id req.json')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveSelfCircs','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrieveSelfCircHistory',
+ '/self/checkouts/history',
+ 'get',
+ 'Historical Circs for logged in user',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'circulation_history',
+ 'eg_auth_token eg_user_id param.limit param.offset param.sort param.before param.after'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value) VALUES
+ ('retrieveSelfCircHistory','limit','query','integer',NULL,NULL),
+ ('retrieveSelfCircHistory','offset','query','integer',NULL,'0'),
+ ('retrieveSelfCircHistory','sort','query','string',NULL,'desc'),
+ ('retrieveSelfCircHistory','before','query','string','date-time',NULL),
+ ('retrieveSelfCircHistory','after','query','string','date-time',NULL)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveSelfCircHistory','array','object') ON CONFLICT DO NOTHING;
+
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrieveSelfCirc', '/self/checkout/:id', 'get', 'Retrieve one circulation for the logged in user', 'open-ils.actor', 'open-ils.actor.user.transaction.fleshed.retrieve', 'eg_auth_token param.id'),
+ ('renewSelfCirc', '/self/checkout/:id', 'put', 'Renew one circulation for the logged in user', 'OpenILS::OpenAPI::Controller::patron', 'renew_circ', 'eg_auth_token param.id eg_user_id'),
+ ('checkinSelfCirc', '/self/checkout/:id', 'delete', 'Check in one circulation for the logged in user', 'OpenILS::OpenAPI::Controller::patron', 'checkin_circ', 'eg_auth_token param.id eg_user_id')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES
+ ('retrieveSelfCirc','id','path','integer',TRUE),
+ ('renewSelfCirc','id','path','integer',TRUE),
+ ('checkinSelfCirc','id','path','integer',TRUE)
+ON CONFLICT DO NOTHING;
+
+-- bib, item, and org methods
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrieveOrgList',
+ '/org_units',
+ 'get',
+ 'List of org units',
+ 'OpenILS::OpenAPI::Controller::org',
+ 'flat_org_list',
+ 'every_param.field every_param.comparison every_param.value'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES
+ ('retrieveOrgList','field','query','string'),
+ ('retrieveOrgList','comparison','query','string'),
+ ('retrieveOrgList','value','query','string')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveOrgList','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrieveOneOrg',
+ '/org_unit/:id',
+ 'get',
+ 'One org unit',
+ 'OpenILS::OpenAPI::Controller::org',
+ 'one_org',
+ 'param.id'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveOneOrg','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveOneOrg','aou') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name) VALUES (
+ 'retrieveFullOrgTree',
+ '/org_tree',
+ 'get',
+ 'Full hierarchical tree of org units',
+ 'OpenILS::OpenAPI::Controller::org',
+ 'full_tree'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveFullOrgTree','aou') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrievePartialOrgTree',
+ '/org_tree/:id',
+ 'get',
+ 'Partial hierarchical tree of org units starting from a specific org unit',
+ 'OpenILS::OpenAPI::Controller::org',
+ 'one_tree',
+ 'param.id'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrievePartialOrgTree','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrievePartialOrgTree','aou') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'createOneBib',
+ '/bibs',
+ 'post',
+ 'Attempts to create a bibliographic record using MARCXML passed as the request content',
+ 'open-ils.cat',
+ 'open-ils.cat.biblio.record.xml.create',
+ 'eg_auth_token req.text param.sourcename'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES ('createOneBib','sourcename','query','string') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('createOneBib','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'updateOneBib',
+ '/bib/:id',
+ 'put',
+ 'Attempts to update a bibliographic record using MARCXML passed as the request content',
+ 'open-ils.cat',
+ 'open-ils.cat.biblio.record.marc.replace',
+ 'eg_auth_token param.id req.text param.sourcename'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('updateOneBib','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES ('updateOneBib','sourcename','query','string') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('updateOneBib','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'deleteOneBib',
+ '/bib/:id',
+ 'delete',
+ 'Attempts to delete a bibliographic record',
+ 'open-ils.cat',
+ 'open-ils.cat.biblio.record_entry.delete',
+ 'eg_auth_token param.id'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('deleteOneBib','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,validate) VALUES ('deleteOneBib','integer',FALSE) ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'updateBREParts',
+ '/bib/:id',
+ 'patch',
+ 'Attempts to update the biblio.record_entry metadata surrounding a bib record',
+ 'OpenILS::OpenAPI::Controller::bib',
+ 'update_bre_parts',
+ 'eg_auth_token param.id req.json'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('updateBREParts','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('updateBREParts','bre') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrieveOneBib',
+ '/bib/:id',
+ 'get',
+ 'Retrieve a bibliographic record, either full biblio::record_entry object, or just the MARCXML',
+ 'OpenILS::OpenAPI::Controller::bib',
+ 'fetch_one_bib',
+ 'param.id'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveOneBib','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveOneBib','bre') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,content_type) VALUES ('retrieveOneBib','application/xml') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,content_type) VALUES ('retrieveOneBib','application/octet-stream') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrieveOneBibHoldings',
+ '/bib/:id/holdings',
+ 'get',
+ 'Retrieve the holdings data for a bibliographic record',
+ 'OpenILS::OpenAPI::Controller::bib',
+ 'fetch_one_bib_holdings',
+ 'param.id'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,default_value,required) VALUES
+ ('retrieveOneBibHoldings','id','path','integer',NULL,TRUE),
+ ('retrieveOneBibHoldings','limit','query','integer',NULL,FALSE),
+ ('retrieveOneBibHoldings','offset','query','integer','0',FALSE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveOneBibHoldings','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'bibDisplayFields',
+ '/bib/:id/display_fields',
+ 'get',
+ 'Retrieve display-related data for a bibliographic record',
+ 'OpenILS::OpenAPI::Controller::bib',
+ 'fetch_one_bib_display_fields',
+ 'param.id req.text'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('bibDisplayFields','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('bibDisplayFields','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'newItems',
+ '/items/fresh',
+ 'get',
+ 'Retrieve a list of newly added items',
+ 'OpenILS::OpenAPI::Controller::bib',
+ 'fetch_new_items',
+ 'param.limit param.offset param.maxage'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,default_value) VALUES ('newItems','limit','query','integer','0') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,default_value) VALUES ('newItems','offset','query','integer','100') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format) VALUES ('newItems','maxage','query','string','interval') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('newItems','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrieveItem',
+ '/item/:barcode',
+ 'get',
+ 'Retrieve one item by its barcode',
+ 'OpenILS::OpenAPI::Controller::bib',
+ 'item_by_barcode',
+ 'param.barcode'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveItem','barcode','path','string',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,description,status,schema_type) VALUES ('retrieveItem','Item Lookup Failed','404','object') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveItem','acp') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'createItem',
+ '/items',
+ 'post',
+ 'Create an item record',
+ 'OpenILS::OpenAPI::Controller::bib',
+ 'create_or_update_one_item',
+ 'eg_auth_token req.json'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,description,status) VALUES ('createItem','Item Creation Failed','400') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('createItem','acp') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'updateItem',
+ '/item/:barcode',
+ 'patch',
+ 'Update a restricted set of item record fields',
+ 'OpenILS::OpenAPI::Controller::bib',
+ 'create_or_update_one_item',
+ 'eg_auth_token req.json param.barcode'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('updateItem','barcode','path','string',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,description,status) VALUES ('updateItem','Item Update Failed','400') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('updateItem','acp') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'deleteItem',
+ '/item/:barcode',
+ 'delete',
+ 'Delete one item record',
+ 'OpenILS::OpenAPI::Controller::bib',
+ 'delete_one_item',
+ 'eg_auth_token param.barcode'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('deleteItem','barcode','path','string',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,description,status,schema_type) VALUES ('deleteItem','Item Deletion Failed','404','object') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('deleteItem','boolean') ON CONFLICT DO NOTHING;
+
+
+-- === patron (non-self) methods
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'searchPatrons',
+ '/patrons',
+ 'get',
+ 'List of patrons matching requested conditions',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'find_users',
+ 'eg_auth_token every_param.field every_param.comparison every_param.value param.limit param.offset'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES
+ ('searchPatrons','field','query','string'),
+ ('searchPatrons','comparison','query','string'),
+ ('searchPatrons','value','query','string')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value) VALUES
+ ('searchPatrons','offset','query','integer',NULL,'0'),
+ ('searchPatrons','limit','query','integer',NULL,'100')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('searchPatrons','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'verifyUserCredentials',
+ '/patrons/verify',
+ 'get',
+ 'Verify the credentials for a user account',
+ 'open-ils.actor',
+ 'open-ils.actor.verify_user_password',
+ 'eg_auth_token param.barcode param.usrname "" param.password'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,required) VALUES
+ ('verifyUserCredentials','barcode','query','string',NULL,FALSE),
+ ('verifyUserCredentials','usrname','query','string',NULL,FALSE),
+ ('verifyUserCredentials','password','query','string','password',TRUE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('verifyUserCredentials','boolean') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrievePatronProfile', '/patron/:userid', 'get', 'Return patron/user record for the requested user', 'OpenILS::OpenAPI::Controller::patron', 'deliver_user', 'eg_auth_token param.userid')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrievePatronProfile','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrievePatronProfile','au') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronIdByCardBarcode',
+ '/patrons/by_barcode/:barcode/id',
+ 'get',
+ 'Retrieve patron id by barcode',
+ 'open-ils.actor',
+ 'open-ils.actor.user.retrieve_id_by_barcode_or_username',
+ 'eg_auth_token param.barcode'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronIdByCardBarcode','barcode','path','string',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('patronIdByCardBarcode','integer') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronIdByUsername',
+ '/patrons/by_username/:username/id',
+ 'get',
+ 'Retrieve patron id by username',
+ 'open-ils.actor',
+ 'open-ils.actor.user.retrieve_id_by_barcode_or_username',
+ 'eg_auth_token "" param.username'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronIdByUsername','username','path','string',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('patronIdByUsername','integer') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronByCardBarcode',
+ '/patrons/by_barcode/:barcode',
+ 'get',
+ 'Retrieve patron by barcode',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'user_by_identifier_string',
+ 'eg_auth_token param.barcode'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronByCardBarcode','barcode','path','string',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('patronByCardBarcode','au') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronByUsername',
+ '/patrons/by_username/:username',
+ 'get',
+ 'Retrieve patron by username',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'user_by_identifier_string',
+ 'eg_auth_token "" param.username'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronByUsername','username','path','string',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('patronByUsername','au') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrievePatronCircHistory',
+ '/patron/:userid/checkouts/history',
+ 'get',
+ 'Historical Circs for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'circulation_history',
+ 'eg_auth_token param.userid param.limit param.offset param.sort param.before param.after'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value,required) VALUES
+ ('retrievePatronCircHistory','userid','path','integer',NULL,NULL,TRUE),
+ ('retrievePatronCircHistory','limit','query','integer',NULL,NULL,FALSE),
+ ('retrievePatronCircHistory','offset','query','integer',NULL,'0',FALSE),
+ ('retrievePatronCircHistory','sort','query','string',NULL,'desc',FALSE),
+ ('retrievePatronCircHistory','before','query','string','date-time',NULL,FALSE),
+ ('retrievePatronCircHistory','after','query','string','date-time',NULL,FALSE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrievePatronCircHistory','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrievePatronHolds', '/patron/:userid/holds', 'get', 'Retrieve unfilled holds for a patron', 'OpenILS::OpenAPI::Controller::hold', 'open_holds', 'eg_auth_token param.userid'),
+ ('requestPatronHold', '/patron/:userid/holds', 'post', 'Request a hold for a patron', 'OpenILS::OpenAPI::Controller::hold', 'request_hold', 'eg_auth_token param.userid req.json'),
+ ('retrievePatronHold','/patron/:userid/hold/:hold', 'get', 'Retrieve one hold for a patron', 'OpenILS::OpenAPI::Controller::hold', 'fetch_user_hold', 'eg_auth_token param.userid param.hold'),
+ ('updatePatronHold', '/patron/:userid/hold/:hold', 'patch', 'Update one hold for a patron', 'OpenILS::OpenAPI::Controller::hold', 'update_user_hold', 'eg_auth_token param.userid param.hold req.json'),
+ ('cancelPatronHold', '/patron/:userid/hold/:hold', 'delete', 'Cancel one hold for a patron', 'OpenILS::OpenAPI::Controller::hold', 'cancel_user_hold', 'eg_auth_token param.userid param.hold "6"')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES
+ ('retrievePatronHolds','userid','path','integer',TRUE),
+ ('retrievePatronHold','userid','path','integer',TRUE),
+ ('retrievePatronHold','hold','path','integer',TRUE),
+ ('requestPatronHold','userid','path','integer',TRUE),
+ ('updatePatronHold','userid','path','integer',TRUE),
+ ('updatePatronHold','hold','path','integer',TRUE),
+ ('cancelPatronHold','userid','path','integer',TRUE),
+ ('cancelPatronHold','hold','path','integer',TRUE)
+ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrieveHoldPickupLocations', '/holds/pickup_locations', 'get', 'Retrieve all valid hold/reserve pickup locations', 'OpenILS::OpenAPI::Controller::hold', 'valid_hold_pickup_locations', '')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveHoldPickupLocations','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrieveHold', '/hold/:hold', 'get', 'Retrieve one hold object', 'open-ils.circ', 'open-ils.circ.hold.details.retrieve', 'eg_auth_token param.hold')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveHold','hold','path','integer',TRUE) ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrievePatronXacts',
+ '/patron/:userid/transactions/:state',
+ 'get',
+ 'Produces a list of transactions of the specified user',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'transactions_by_state',
+ 'eg_auth_token param.userid state param.limit param.offset param.sort param.before param.after'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value,required) VALUES
+ ('retrievePatronXacts','userid','path','integer',NULL,NULL,TRUE),
+ ('retrievePatronXacts','state','path','string',NULL,NULL,TRUE),
+ ('retrievePatronXacts','limit','query','integer',NULL,NULL,FALSE),
+ ('retrievePatronXacts','offset','query','integer',NULL,'0',FALSE),
+ ('retrievePatronXacts','sort','query','string',NULL,'desc',FALSE),
+ ('retrievePatronXacts','before','query','string','date-time',NULL,FALSE),
+ ('retrievePatronXacts','after','query','string','date-time',NULL,FALSE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrievePatronXacts','array','object') ON CONFLICT DO NOTHING;
+
+-- general xact detail
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'retrievePatronXact',
+ '/patron/:userid/transaction/:id',
+ 'get',
+ 'Details of one transaction for the specified user',
+ 'open-ils.actor',
+ 'open-ils.actor.user.transaction.fleshed.retrieve',
+ 'eg_auth_token param.id'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrievePatronXact','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrievePatronXact','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrievePatronCircs', '/patron/:userid/checkouts', 'get', 'Open Circs for a patron', 'open-ils.circ', 'open-ils.circ.actor.user.checked_out.atomic', 'eg_auth_token param.userid'),
+ ('requestPatronCirc', '/patron/:userid/checkouts', 'post', 'Attempt a circulation for a patron', 'OpenILS::OpenAPI::Controller::patron', 'checkout_item', 'eg_auth_token param.userid req.json')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrievePatronCircs','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('requestPatronCirc','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrievePatronCircs','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrievePatronCirc', '/patron/:userid/checkout/:id', 'get', 'Retrieve one circulation for the specified user', 'open-ils.actor', 'open-ils.actor.user.transaction.fleshed.retrieve', 'eg_auth_token param.id'),
+ ('renewPatronCirc', '/patron/:userid/checkout/:id', 'put', 'Renew one circulation for the specified user', 'OpenILS::OpenAPI::Controller::patron', 'renew_circ', 'eg_auth_token param.id param.userid'),
+ ('checkinPatronCirc', '/patron/:userid/checkout/:id', 'delete', 'Check in one circulation for the specified user', 'OpenILS::OpenAPI::Controller::patron', 'checkin_circ', 'eg_auth_token param.id param.userid')
+ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES
+ ('retrievePatronCirc','userid','path','integer',TRUE),
+ ('retrievePatronCirc','id','path','integer',TRUE),
+ ('renewPatronCirc','userid','path','integer',TRUE),
+ ('renewPatronCirc','id','path','integer',TRUE),
+ ('checkinPatronCirc','userid','path','integer',TRUE),
+ ('checkinPatronCirc','id','path','integer',TRUE)
+ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronATEvents',
+ '/patron/:userid/triggered_events',
+ 'get',
+ 'Retrieve a list of A/T events for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'usr_at_events',
+ 'eg_auth_token param.userid param.limit param.offset param.before param.after every_param.hook'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value,required) VALUES
+ ('patronATEvents','userid','path','integer',NULL,NULL,TRUE),
+ ('patronATEvents','limit','query','integer',NULL,'100',FALSE),
+ ('patronATEvents','offset','query','integer',NULL,'0',FALSE),
+ ('patronATEvents','before','query','string','date-time',NULL,FALSE),
+ ('patronATEvents','after','query','string','date-time',NULL,FALSE),
+ ('patronATEvents','hook','query','string',NULL,NULL,FALSE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('patronATEvents','array','integer') ON CONFLICT DO NOTHING; -- array of ausp ids
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronATEvent',
+ '/patron/:userid/triggered_event/:eventid',
+ 'get',
+ 'Retrieve one penalty for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'usr_at_events',
+ 'eg_auth_token param.userid "1" "0" "" "" "" param.eventid'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronATEvent','eventid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronATEvent','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronActivePenalties',
+ '/patron/:userid/standing_penalties',
+ 'get',
+ 'Retrieve all penalty details for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'standing_penalties',
+ 'eg_auth_token param.userid'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronActivePenalties','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('patronActivePenalties','array','integer') ON CONFLICT DO NOTHING; -- array of ausp ids
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronPenalty',
+ '/patron/:userid/standing_penalty/:penaltyid',
+ 'get',
+ 'Retrieve one penalty for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'standing_penalties',
+ 'eg_auth_token param.userid "0" param.penaltyid'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronPenalty','penaltyid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronPenalty','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronActiveMessages',
+ '/patron/:userid/messages',
+ 'get',
+ 'Retrieve all active message ids for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'usr_messages',
+ 'eg_auth_token param.userid "0"'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronActiveMessages','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('patronActiveMessages','array','integer') ON CONFLICT DO NOTHING; -- array of aum ids
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronMessage',
+ '/patron/:userid/message/:msgid',
+ 'get',
+ 'Retrieve one message for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'usr_messages',
+ 'eg_auth_token param.userid "0" param.msgid'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessage','msgid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessage','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('patronMessage','aum') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronMessageUpdate',
+ '/patron/:userid/message/:msgid',
+ 'patch',
+ 'Update one message for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'update_usr_message',
+ 'eg_auth_token eg_user_id param.userid param.msgid req.json'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessageUpdate','msgid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessageUpdate','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('patronMessageUpdate','aum') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronMessageArchive',
+ '/patron/:userid/message/:msgid',
+ 'delete',
+ 'Archive one message for a patron',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'archive_usr_message',
+ 'eg_auth_token eg_user_id param.userid param.msgid'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessageArchive','msgid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessageArchive','userid','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('patronMessageArchive','boolean') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'patronActivityLog',
+ '/patron/:userid/activity',
+ 'get',
+ 'Retrieve patron activity (authen, authz, etc)',
+ 'OpenILS::OpenAPI::Controller::patron',
+ 'usr_activity',
+ 'eg_auth_token param.userid param.maxage param.limit param.offset param.sort'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value,required) VALUES
+ ('patronActivityLog','userid','path','integer',NULL,NULL,TRUE),
+ ('patronActivityLog','limit','query','integer',NULL,'100',FALSE),
+ ('patronActivityLog','offset','query','integer',NULL,'0',FALSE),
+ ('patronActivityLog','sort','query','string',NULL,'desc',FALSE),
+ ('patronActivityLog','maxage','query','string','date-time',NULL,FALSE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('patronActivityLog','array','object') ON CONFLICT DO NOTHING;
+
+------- collections
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'collectionsPatronsOfInterest',
+ '/collections/:shortname/users_of_interest',
+ 'get',
+ 'List of patrons to consider for collections based on the search criteria provided.',
+ 'open-ils.collections',
+ 'open-ils.collections.users_of_interest.retrieve',
+ 'eg_auth_token param.fine_age param.fine_amount param.shortname'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,required) VALUES
+ ('collectionsPatronsOfInterest','shortname','path','string',NULL,TRUE),
+ ('collectionsPatronsOfInterest','fine_age','query','integer',NULL,TRUE),
+ ('collectionsPatronsOfInterest','fine_amount','query','string','money',TRUE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items,validate) VALUES ('collectionsPatronsOfInterest','array','object',FALSE) ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'collectionsPatronsOfInterestWarning',
+ '/collections/:shortname/users_of_interest/warning',
+ 'get',
+ 'List of patrons with the PATRON_EXCEEDS_COLLECTIONS_WARNING penalty to consider for collections based on the search criteria provided.',
+ 'open-ils.collections',
+ 'open-ils.collections.users_of_interest.warning_penalty.retrieve',
+ 'eg_auth_token param.shortname param.min_age param.max_age'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,required) VALUES
+ ('collectionsPatronsOfInterestWarning','shortname','path','string',NULL,TRUE),
+ ('collectionsPatronsOfInterestWarning','min_age','query','string','date-time',FALSE),
+ ('collectionsPatronsOfInterestWarning','max_age','query','string','date-time',FALSE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items,validate) VALUES ('collectionsPatronsOfInterestWarning','array','object',FALSE) ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'collectionsGetPatronDetail',
+ '/patron/:usrid/collections/:shortname',
+ 'get',
+ 'Get collections-related transaction details for a patron.',
+ 'open-ils.collections',
+ 'open-ils.collections.user_transaction_details.retrieve',
+ 'eg_auth_token param.start param.end param.shortname every_param.usrid'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,required) VALUES
+ ('collectionsGetPatronDetail','usrid','path','integer',NULL,TRUE),
+ ('collectionsGetPatronDetail','shortname','path','string',NULL,TRUE),
+ ('collectionsGetPatronDetail','start','query','string','date-time',TRUE),
+ ('collectionsGetPatronDetail','end','query','string','date-time',TRUE)
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items,validate) VALUES ('collectionsGetPatronDetail','array','object',FALSE) ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'collectionsPutPatronInCollections',
+ '/patron/:usrid/collections/:shortname',
+ 'post',
+ 'Put patron into collections.',
+ 'open-ils.collections',
+ 'open-ils.collections.put_into_collections',
+ 'eg_auth_token param.usrid param.shortname param.fee param.note'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,required) VALUES
+ ('collectionsPutPatronInCollections','usrid','path','integer',NULL,TRUE),
+ ('collectionsPutPatronInCollections','shortname','path','string',NULL,TRUE),
+ ('collectionsPutPatronInCollections','fee','query','string','money',FALSE),
+ ('collectionsPutPatronInCollections','note','query','string',NULL,FALSE)
+ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES (
+ 'collectionsRemovePatronFromCollections',
+ '/patron/:usrid/collections/:shortname',
+ 'delete',
+ 'Remove patron from collections.',
+ 'open-ils.collections',
+ 'open-ils.collections.remove_from_collections',
+ 'eg_auth_token param.usrid param.shortname'
+) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES
+ ('collectionsRemovePatronFromCollections','usrid','path','integer',TRUE),
+ ('collectionsRemovePatronFromCollections','shortname','path','string',TRUE)
+ON CONFLICT DO NOTHING;
+
+
+------- courses
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('activeCourses', '/courses', 'get', 'Retrieve all courses used for course material reservation', 'OpenILS::OpenAPI::Controller::course', 'get_active_courses', 'eg_auth_token every_param.org')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES ('activeCourses','org','query','integer') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('activeCourses','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('activeRoles', '/courses/public_role_users', 'get', 'Retrieve all public roles used for courses', 'OpenILS::OpenAPI::Controller::course', 'get_all_course_public_roles', 'eg_auth_token every_param.org')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES ('activeRoles','org','query','integer') ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('activeRoles','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrieveCourse', '/course/:id', 'get', 'Retrieve one detailed course', 'OpenILS::OpenAPI::Controller::course', 'get_course_detail', 'eg_auth_token param.id')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveCourse','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveCourse','acmc') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrieveCourseMaterials', '/course/:id/materials', 'get', 'Retrieve detailed materials for one course', 'OpenILS::OpenAPI::Controller::course', 'get_course_materials', 'param.id')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveCourseMaterials','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveCourseMaterials','array','object') ON CONFLICT DO NOTHING;
+
+INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES
+ ('retrieveCourseUsers', '/course/:id/public_role_users', 'get', 'Retrieve detailed user list for one course', 'open-ils.courses', 'open-ils.courses.course_users.retrieve', 'param.id')
+ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveCourseUsers','id','path','integer',TRUE) ON CONFLICT DO NOTHING;
+INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveCourseUsers','array','object') ON CONFLICT DO NOTHING;
+
+--------- put likely stock endpoints into sets --------
+
+INSERT INTO openapi.endpoint_set_endpoint_map (endpoint, endpoint_set)
+ SELECT e.operation_id, s.name FROM openapi.endpoint e JOIN openapi.endpoint_set s ON (e.path LIKE '/'||RTRIM(s.name,'s')||'%')
+ON CONFLICT DO NOTHING;
+
+-- Check that all endpoints are in sets -- should return 0 rows
+SELECT * FROM openapi.endpoint e WHERE NOT EXISTS (SELECT 1 FROM openapi.endpoint_set_endpoint_map WHERE endpoint = e.operation_id);
+
+-- Global Fieldmapper property-filtering org and user settings
+INSERT INTO config.settings_group (name,label) VALUES ('openapi','OpenAPI data access control');
+
+INSERT INTO config.org_unit_setting_type (name,label,grp) VALUES ('REST.api.blacklist_properties','Globally filtered Fieldmapper properties','openapi');
+INSERT INTO config.org_unit_setting_type (name,label,grp) VALUES ('REST.api.whitelist_properties','Globally whitelisted Fieldmapper properties','openapi');
+
+UPDATE config.org_unit_setting_type
+ SET update_perm = (SELECT id FROM permission.perm_list WHERE code = 'ADMIN_OPENAPI' LIMIT 1)
+ WHERE name IN ('REST.api.blacklist_properties','REST.api.whitelist_properties');
+
+INSERT INTO config.usr_setting_type (name,label,grp) VALUES ('REST.api.whitelist_properties','Globally whitelisted Fieldmapper properties','openapi');
+INSERT INTO config.usr_setting_type (name,label,grp) VALUES ('REST.api.blacklist_properties','Globally filtered Fieldmapper properties','openapi');
+
+
+/* -- Some extra example permission setup, to allow (basically) readonly patron retrieve
+
+INSERT INTO permission.perm_list (code,description) VALUES ('REST.api.patrons.detail.read','Permission meant to facilitate read-only patron-related API access');
+INSERT INTO openapi.perm_set (name) values ('Patron Detail, Readonly');
+INSERT INTO openapi.perm_set_perm_map (perm_set,perm) SELECT s.id, p.id FROM openapi.perm_set s, permission.perm_list p WHERE p.code IN ('REST.api', 'REST.api.patrons.detail.read') AND s.name='Patron RO';
+INSERT INTO openapi.endpoint_perm_set_map (endpoint,perm_set) SELECT 'retrievePatronProfile', s.id FROM openapi.perm_set s WHERE s.name='Patron RO';
+
+*/
+COMMIT;
+
+-- Update auditor tables to catch changes to source tables.
+-- Can be removed/skipped if there were no schema changes.
+SELECT auditor.update_auditors();
diff --git a/README b/README
deleted file mode 120000
index 1486a8d676..0000000000
--- a/README
+++ /dev/null
@@ -1 +0,0 @@
-docs/modules/installation/pages/server_installation.adoc
\ No newline at end of file
diff --git a/README b/README
new file mode 100644
index 0000000000..073d193c82
--- /dev/null
+++ b/README
@@ -0,0 +1,825 @@
+= Installing the Evergreen server =
+:toc:
+
+== Preamble: referenced user accounts ==
+
+In subsequent sections, we will refer to a number of different accounts, as
+follows:
+
+ * Linux user accounts:
+ ** The *user* Linux account is the account that you use to log onto the
+ Linux system as a regular user.
+ ** The *root* Linux account is an account that has system administrator
+ privileges. On Debian you can switch to this account from
+ your *user* account by issuing the `su -` command and entering the
+ password for the *root* account when prompted. On Ubuntu you can switch
+ to this account from your *user* account using the `sudo su -` command
+ and entering the password for your *user* account when prompted.
+ ** The *opensrf* Linux account is an account that you create when installing
+ OpenSRF. You can switch to this account from the *root* account by
+ issuing the `su - opensrf` command.
+ ** The *postgres* Linux account is created automatically when you install
+ the PostgreSQL database server. You can switch to this account from the
+ *root* account by issuing the `su - postgres` command.
+ * PostgreSQL user accounts:
+ ** The *evergreen* PostgreSQL account is a superuser account that you will
+ create to connect to the PostgreSQL database server.
+ * Evergreen administrator account:
+ ** The *egadmin* Evergreen account is an administrator account for
+ Evergreen that you will use to test connectivity and configure your
+ Evergreen instance.
+
+== Preamble: developer instructions ==
+
+[NOTE]
+Skip this section if you are using an official release tarball downloaded
+from http://evergreen-ils.org/egdownloads
+
+Developers working directly with the source code from the Git repository,
+rather than an official release tarball, must perform one step before they
+can proceed with the `./configure` step.
+
+As the *user* Linux account, issue the following command in the Evergreen
+source directory to generate the configure script and Makefiles:
+
+[source, bash]
+------------------------------------------------------------------------------
+autoreconf -i
+------------------------------------------------------------------------------
+
+== Installing prerequisites ==
+
+ * **PostgreSQL**: The minimum supported version is 13.
+ * **Linux**: Evergreen has been tested on
+ Debian Bookworm (12),
+ Debian Bullseye (11),
+ Debian Buster (10),
+ Ubuntu Noble Numbat (24.04),
+ and Ubuntu Jammy Jellyfish (22.04).
+ If you are running an older version of these distributions, you may want
+ to upgrade before upgrading Evergreen. For instructions on upgrading these
+ distributions, visit the Debian or Ubuntu websites.
+ * **OpenSRF**: The minimum supported version of OpenSRF is 3.3.0.
+
+
+Evergreen has a number of prerequisite packages that must be installed
+before you can successfully configure, compile, and install Evergreen.
+
+1. Begin by installing the most recent version of OpenSRF (3.3.0 or later).
+ You can download OpenSRF releases from http://evergreen-ils.org/opensrf-downloads/
++
+2. Issue the following commands as the *root* Linux account to install
+ prerequisites using the `Makefile.install` prerequisite installer,
+ substituting `debian-bookworm`,`debian-bullseye`,`debian-buster`,`ubuntu-jammy`,
+ or `ubuntu-noble` for <osname> below:
++
+[source, bash]
+------------------------------------------------------------------------------
+make -f Open-ILS/src/extras/Makefile.install <osname>
+------------------------------------------------------------------------------
++
+[[optional_developer_additions]]
+3. OPTIONAL: Developer additions
++
+To perform certain developer tasks from a Git source code checkout,
+additional packages are required. As the *root* Linux account:
++
+ * To install packages needed for retrieving and managing web dependencies,
+ use the <osname>-developer Makefile.install target. Currently,
+ this is only needed for building and installing the web
+ staff client.
+ * To optionally run Angular and AngularJS tests you will need to manually
+ install Firefox and your choice of Chromium or Chrome.
++
+[source, bash]
+------------------------------------------------------------------------------
+make -f Open-ILS/src/extras/Makefile.install <osname>-developer
+------------------------------------------------------------------------------
++
+ * To install packages required for building Evergreen translations, use
+ the <osname>-translator Makefile.install target.
++
+[source, bash]
+------------------------------------------------------------------------------
+make -f Open-ILS/src/extras/Makefile.install <osname>-translator
+------------------------------------------------------------------------------
++
+ * To install packages required for building Evergreen release bundles, use
+ the <osname>-packager Makefile.install target.
++
+[source, bash]
+------------------------------------------------------------------------------
+make -f Open-ILS/src/extras/Makefile.install <osname>-packager
+------------------------------------------------------------------------------
+
+== Extra steps for web staff client ==
+
+[NOTE]
+Skip this entire section if you are using an official release tarball downloaded
+from http://evergreen-ils.org/downloads. Otherwise, ensure you have installed the
+xref:#optional_developer_additions[optional developer additions] before proceeding.
+
+[[install_files_for_web_staff_client]]
+=== Install AngularJS files for web staff client ===
+
+1. Building, Testing, Minification: The remaining steps all take place within
+ the staff JS web root:
++
+[source,sh]
+------------------------------------------------------------------------------
+cd $EVERGREEN_ROOT/Open-ILS/web/js/ui/default/staff/
+------------------------------------------------------------------------------
++
+2. Install Project-local Dependencies. npm inspects the 'package.json' file
+ for dependencies and fetches them from the Node package network.
++
+[source,sh]
+------------------------------------------------------------------------------
+npm ci # fetch JS dependencies
+------------------------------------------------------------------------------
++
+3. Run the build script.
++
+[source,sh]
+------------------------------------------------------------------------------
+npm run build-prod
+------------------------------------------------------------------------------
++
+4. OPTIONAL: Test web client code if the <osname>-developer packages and
+ the necessary browsers are installed.
+ CHROME_BIN should be set to the path to chrome or chromimum, e.g.,
+ `/usr/bin/chromium`:
++
+[source,sh]
+------------------------------------------------------------------------------
+CHROME_BIN=/path/to/chrome npm run test
+------------------------------------------------------------------------------
+
+[[install_files_for_angular_web_staff_client]]
+=== Install Angular files for web staff client ===
+
+1. Building, Testing, Minification: The remaining steps all take place within
+ the Angular staff root:
++
+[source,sh]
+------------------------------------------------------------------------------
+cd $EVERGREEN_ROOT/Open-ILS/src/eg2/
+------------------------------------------------------------------------------
++
+2. Install Project-local Dependencies. npm inspects the 'package.json' file
+ for dependencies and fetches them from the Node package network.
++
+[source,sh]
+------------------------------------------------------------------------------
+npm ci # fetch JS dependencies
+------------------------------------------------------------------------------
++
+3. Run the build script.
++
+[source,sh]
+------------------------------------------------------------------------------
+ng build --configuration=production
+------------------------------------------------------------------------------
++
+This can be a memory-intensive build. If the process does not finish, and you
+get the message "Killed" in the console, try running it with
+https://nodejs.org/api/cli.html#cli_max_old_space_size_size_in_megabytes[an explicit max-old-space-size option]
+to encourage more garbage collection. For example, on a machine with 4GB of
+memory, you can limit max-old-space-size to 3GB with:
++
+[source,sh]
+------------------------------------------------------------------------------
+NODE_OPTIONS=--max-old-space-size=3072 ng build --configuration=production
+------------------------------------------------------------------------------
++
+4. OPTIONAL: Test eg2 web client code if the <osname>-developer packages and
+ the necessary browsers are installed:
+ CHROME_BIN should be set to the path to chrome or chromimum, e.g.,
+ `/usr/bin/chromium`:
++
+[source,sh]
+------------------------------------------------------------------------------
+CHROME_BIN=/path/to/chrome npm run test
+MOZ_HEADLESS=1 ng e2e
+------------------------------------------------------------------------------
+
+[[install_opac_deps]]
+=== Install OPAC skin dependencies ===
+
+1. The following steps take place within the OPAC dependencies root:
++
+[source,sh]
+------------------------------------------------------------------------------
+cd $EVERGREEN_ROOT/Open-ILS/web/opac/deps
+------------------------------------------------------------------------------
++
+2. Install Project-local Dependencies. npm inspects the 'package.json' file
+ for dependencies and fetches them from the Node package network.
++
+[source,sh]
+------------------------------------------------------------------------------
+npm ci # fetch JS and CSS dependencies
+------------------------------------------------------------------------------
++
+Note that there is no build step.
+
+3. OPTIONAL: Test OPAC javascript code:
++
+[source,sh]
+------------------------------------------------------------------------------
+npm run test
+------------------------------------------------------------------------------
+
+
+== Configuration and compilation instructions ==
+
+For the time being, we are still installing everything in the `/openils/`
+directory. From the Evergreen source directory, issue the following commands as
+the *user* Linux account to configure and build Evergreen:
+
+[source, bash]
+------------------------------------------------------------------------------
+PATH=/openils/bin:$PATH ./configure --prefix=/openils --sysconfdir=/openils/conf
+make
+------------------------------------------------------------------------------
+
+These instructions assume that you have also installed OpenSRF under `/openils/`.
+If not, please adjust PATH as needed so that the Evergreen `configure` script
+can find `osrf_config`.
+
+== Installation instructions ==
+
+1. Once you have configured and compiled Evergreen, issue the following
+ command as the *root* Linux account to install Evergreen and copy
+ example configuration files to `/openils/conf`.
++
+[source, bash]
+------------------------------------------------------------------------------
+make install
+------------------------------------------------------------------------------
+
+== Change ownership of the Evergreen files ==
+
+All files in the `/openils/` directory and subdirectories must be owned by the
+`opensrf` user. Issue the following command as the *root* Linux account to
+change the ownership on the files:
+
+[source, bash]
+------------------------------------------------------------------------------
+chown -R opensrf:opensrf /openils
+------------------------------------------------------------------------------
+
+== Run ldconfig ==
+
+On Ubuntu or Debian, run the following command as the root user:
+
+[source, bash]
+------------------------------------------------------------------------------
+ldconfig
+------------------------------------------------------------------------------
+
+== Additional Instructions for Developers ==
+
+[NOTE]
+Skip this section if you are using an official release tarball downloaded
+from http://evergreen-ils.org/egdownloads
+
+Developers working directly with the source code from the Git repository,
+rather than an official release tarball, need to install the Dojo Toolkit
+set of JavaScript libraries. The appropriate version of Dojo is included in
+Evergreen release tarballs. Developers should install the Dojo 1.3.3 version
+of Dojo by issuing the following commands as the *opensrf* Linux account:
+
+[source, bash]
+------------------------------------------------------------------------------
+wget https://download.dojotoolkit.org/release-1.3.3/dojo-release-1.3.3.tar.gz
+tar -C /openils/var/web/js -xzf dojo-release-1.3.3.tar.gz
+cp -r /openils/var/web/js/dojo-release-1.3.3/* /openils/var/web/js/dojo/.
+------------------------------------------------------------------------------
+
+
+== Configure the Apache Web server ==
+
+. Use the example configuration files to configure your Web server for
+the Evergreen catalog, web staff client, Web services, and administration
+interfaces. Issue the following commands as the *root* Linux account:
++
+[source,bash]
+------------------------------------------------------------------------------------
+cp Open-ILS/examples/apache_24/eg_24.conf /etc/apache2/sites-available/eg.conf
+cp Open-ILS/examples/apache_24/eg_vhost_24.conf /etc/apache2/eg_vhost.conf
+cp Open-ILS/examples/apache_24/eg_startup /etc/apache2/
+# Now set up SSL
+mkdir /etc/apache2/ssl
+cd /etc/apache2/ssl
+------------------------------------------------------------------------------------
++
+. The `openssl` command cuts a new SSL key for your Apache server. For a
+production server, you should purchase a signed SSL certificate, but you can
+just use a self-signed certificate and accept the warnings in the
+and browser during testing and development. Create an SSL key for the Apache
+server by issuing the following command as the *root* Linux account:
++
+[source,bash]
+------------------------------------------------------------------------------
+openssl req -new -x509 -days 365 -nodes -out server.crt -keyout server.key
+------------------------------------------------------------------------------
++
+. As the *root* Linux account, edit the `eg.conf` file that you copied into
+place.
+ a. To enable access to the offline upload / execute interface from any
+ workstation on any network, make the following change (and note that
+ you *must* secure this for a production instance):
+ * Replace `Require host 10.0.0.0/8` with `Require all granted`
+. Change the user for the Apache server.
+ * As the *root* Linux account, edit
+ `/etc/apache2/envvars`. Change `export APACHE_RUN_USER=www-data` to
+ `export APACHE_RUN_USER=opensrf`.
+. As the *root* Linux account, configure Apache with KeepAlive settings
+ appropriate for Evergreen. Higher values can improve the performance of a
+ single client by allowing multiple requests to be sent over the same TCP
+ connection, but increase the risk of using up all available Apache child
+ processes and memory.
+ * Edit `/etc/apache2/apache2.conf`.
+ a. Change `KeepAliveTimeout` to `1`.
+ b. Change `MaxKeepAliveRequests` to `100`.
+. As the *root* Linux account, configure the prefork module to start and keep
+ enough Apache servers available to provide quick responses to clients without
+ running out of memory. The following settings are a good starting point for a
+ site that exposes the default Evergreen catalog to the web:
++
+.`/etc/apache2/mods-available/mpm_prefork.conf`
+[source,bash]
+------------------------------------------------------------------------------
+<IfModule mpm_prefork_module>
+ StartServers 15
+ MinSpareServers 5
+ MaxSpareServers 15
+ MaxRequestWorkers 75
+ MaxConnectionsPerChild 500
+</IfModule>
+------------------------------------------------------------------------------
++
+. As the *root* user, enable the mpm_prefork module:
++
+[source,bash]
+------------------------------------------------------------------------------
+a2dismod mpm_event
+a2enmod mpm_prefork
+------------------------------------------------------------------------------
++
+. As the *root* Linux account, enable the Evergreen site:
++
+[source,bash]
+------------------------------------------------------------------------------
+a2dissite 000-default # OPTIONAL: disable the default site (the "It Works" page)
+a2ensite eg.conf
+------------------------------------------------------------------------------
++
+. As the *root* Linux account, enable Apache to write
+ to the lock directory; this is currently necessary because Apache
+ is running as the `opensrf` user:
++
+[source,bash]
+------------------------------------------------------------------------------
+chown opensrf /var/lock/apache2
+------------------------------------------------------------------------------
+
+Learn more about additional Apache options in the following sections:
+
+ * xref:admin:apache_rewrite_tricks.adoc#apache_rewrite_tricks[Apache Rewrite Tricks]
+ * xref:admin:apache_access_handler.adoc#apache_access_handler_perl_module[Apache Access Handler Perl Module]
+
+== Configure OpenSRF for the Evergreen application ==
+
+There are a number of example OpenSRF configuration files in `/openils/conf/`
+that you can use as a template for your Evergreen installation. Issue the
+following commands as the *opensrf* Linux account:
+
+[source, bash]
+------------------------------------------------------------------------------
+cp -b /openils/conf/opensrf_core.xml.example /openils/conf/opensrf_core.xml
+cp -b /openils/conf/opensrf.xml.example /openils/conf/opensrf.xml
+------------------------------------------------------------------------------
+
+When you installed OpenSRF, you created four Jabber users on two
+separate domains and edited the `opensrf_core.xml` file accordingly. Please
+refer back to the OpenSRF README and, as the *opensrf* Linux account, edit the
+Evergreen version of the `opensrf_core.xml` file using the same Jabber users
+and domains as you used while installing and testing OpenSRF.
+
+=== OPTIONAL: Configure Evergreen for OpenSRF+Redis
+
+If using the Redis variant of OpenSRF, modify /openils/conf/opensrf_core.xml
+to use the Redis settings instead of the Ejabberd settings.
+
+Several sections of the file have 2 configuration blocks, one for Ejabberd
+and one for Redis. Example:
+
+[source,xml]
+------------------------------------------------------------------------------
+<!-- Ejabberd -->
+<passwd>password</passwd>
+<port>5222</port>
+<!-- === -->
+
+<!-- Redis -->
+<!--
+<passwd>456fc340-beba-4489-9070-0d6b49e9952b</passwd>
+<port>6379</port>
+-->
+<!-- === -->
+------------------------------------------------------------------------------
+
+For each occurrence of such block, commente out the Ejabberd sections
+and un-comment the Redis sections. Example:
+
+[source,xml]
+------------------------------------------------------------------------------
+<!-- Ejabberd -->
+<!--
+<passwd>password</passwd>
+<port>5222</port>
+-->
+<!-- === -->
+
+<!-- Redis -->
+<passwd>456fc340-beba-4489-9070-0d6b49e9952b</passwd>
+<port>6379</port>
+<!-- === -->
+------------------------------------------------------------------------------
+
+[NOTE]
+The `-b` flag tells the `cp` command to create a backup version of the
+destination file. The backup version of the destination file has a tilde (`~`)
+appended to the file name, so if you have forgotten the Jabber users and
+domains, you can retrieve the settings from the backup version of the files.
+
+`eg_db_config`, described in xref:#creating_the_evergreen_database[Creating the Evergreen database], sets the database connection information in `opensrf.xml` for you.
+
+=== Configure action triggers for the Evergreen application ===
+_Action Triggers_ provide hooks for the system to perform actions when a given
+event occurs; for example, to generate reminder or overdue notices, the
+`checkout.due` hook is processed and events are triggered for potential actions
+if there is no checkin time.
+
+To enable the default set of hooks, issue the following command as the
+*opensrf* Linux account:
+
+[source, bash]
+------------------------------------------------------------------------------
+cp -b /openils/conf/action_trigger_filters.json.example /openils/conf/action_trigger_filters.json
+------------------------------------------------------------------------------
+
+For more information about configuring and running action triggers, see
+xref:admin:actiontriggers.adoc#processing_action_triggers[Notifications / Action Triggers].
+
+[[creating_the_evergreen_database]]
+== Creating the Evergreen database ==
+
+=== Setting up the PostgreSQL server ===
+
+For production use, most libraries install the PostgreSQL database server on a
+dedicated machine. Therefore, by default, the `Makefile.install` prerequisite
+installer does *not* install the PostgreSQL database server that is required
+by every Evergreen system. You can install the packages required by Debian or
+Ubuntu on the machine of your choice using the following commands as the
+*root* Linux account:
+
+.Installing PostgreSQL server packages
+
+Each OS build target provides the postgres server installation
+packages required for each operating system. To install Postgres
+server packages, use the make target
+'postgres-server-<OSTYPE>-<POSTGRESVERSION>'. Choose the most
+appropriate command below based on your operating system and desired
+PostgreSQL Version.
+
+To install PostgreSQL version 13, use the following command for your operating
+system:
+
+[WARNING]
+=========
+PostgreSQL 12+ includes a feature called "JIT" (Just-in-Time compilation).
+Do not turn on Postgres' JIT capabilities. Evergreen's queries, especially complex
+ones used for search, are intentionally tuned for non-JIT execution and JIT has
+been shown to be harmful in some circumstances.
+Recommended minimum tweak to postgresql.conf:
+jit_above_cost = -1
+=========
+
+[source, bash]
+------------------------------------------------------------------------------
+make -f Open-ILS/src/extras/Makefile.install postgres-server-debian-bookworm-13
+make -f Open-ILS/src/extras/Makefile.install postgres-server-debian-bullseye-13
+make -f Open-ILS/src/extras/Makefile.install postgres-server-debian-buster-13
+make -f Open-ILS/src/extras/Makefile.install postgres-server-ubuntu-jammy-13
+make -f Open-ILS/src/extras/Makefile.install postgres-server-ubuntu-noble-13
+------------------------------------------------------------------------------
+
+To install PostgreSQL version 14, use the following command for your operating
+system:
+
+[WARNING]
+=========
+PostgreSQL 12+ includes a feature called "JIT" (Just-in-Time compilation).
+Do not turn on Postgres' JIT capabilities. Evergreen's queries, especially complex
+ones used for search, are intentionally tuned for non-JIT execution and JIT has
+been shown to be harmful in some circumstances.
+Recommended minimum tweak to postgresql.conf:
+jit_above_cost = -1
+=========
+
+[source, bash]
+------------------------------------------------------------------------------
+make -f Open-ILS/src/extras/Makefile.install postgres-server-debian-bookworm-14
+make -f Open-ILS/src/extras/Makefile.install postgres-server-debian-bullseye-14
+make -f Open-ILS/src/extras/Makefile.install postgres-server-debian-buster-14
+make -f Open-ILS/src/extras/Makefile.install postgres-server-ubuntu-jammy-14
+make -f Open-ILS/src/extras/Makefile.install postgres-server-ubuntu-noble-14
+------------------------------------------------------------------------------
+
+To install PostgreSQL version 15, use the following command for your operating
+system:
+
+[WARNING]
+=========
+PostgreSQL 12+ includes a feature called "JIT" (Just-in-Time compilation).
+Do not turn on Postgres' JIT capabilities. Evergreen's queries, especially complex
+ones used for search, are intentionally tuned for non-JIT execution and JIT has
+been shown to be harmful in some circumstances.
+Recommended minimum tweak to postgresql.conf:
+jit_above_cost = -1
+=========
+
+[source, bash]
+------------------------------------------------------------------------------
+make -f Open-ILS/src/extras/Makefile.install postgres-server-debian-bookworm-15
+make -f Open-ILS/src/extras/Makefile.install postgres-server-debian-bullseye-15
+make -f Open-ILS/src/extras/Makefile.install postgres-server-debian-buster-15
+make -f Open-ILS/src/extras/Makefile.install postgres-server-ubuntu-jammy-15
+make -f Open-ILS/src/extras/Makefile.install postgres-server-ubuntu-noble-15
+------------------------------------------------------------------------------
+
+To install PostgreSQL version 16, use the following command for your operating
+system:
+
+[WARNING]
+=========
+PostgreSQL 12+ includes a feature called "JIT" (Just-in-Time compilation).
+Do not turn on Postgres' JIT capabilities. Evergreen's queries, especially complex
+ones used for search, are intentionally tuned for non-JIT execution and JIT has
+been shown to be harmful in some circumstances.
+Recommended minimum tweak to postgresql.conf:
+jit_above_cost = -1
+=========
+
+[source, bash]
+------------------------------------------------------------------------------
+make -f Open-ILS/src/extras/Makefile.install postgres-server-debian-bookworm-16
+make -f Open-ILS/src/extras/Makefile.install postgres-server-debian-bullseye-16
+make -f Open-ILS/src/extras/Makefile.install postgres-server-debian-buster-16
+make -f Open-ILS/src/extras/Makefile.install postgres-server-ubuntu-jammy-16
+make -f Open-ILS/src/extras/Makefile.install postgres-server-ubuntu-noble-16
+------------------------------------------------------------------------------
+
+To install PostgreSQL version 17, use the following command for your operating
+system:
+
+[WARNING]
+=========
+PostgreSQL 12+ includes a feature called "JIT" (Just-in-Time compilation).
+Do not turn on Postgres' JIT capabilities. Evergreen's queries, especially complex
+ones used for search, are intentionally tuned for non-JIT execution and JIT has
+been shown to be harmful in some circumstances.
+Recommended minimum tweak to postgresql.conf:
+jit_above_cost = -1
+=========
+
+[source, bash]
+------------------------------------------------------------------------------
+make -f Open-ILS/src/extras/Makefile.install postgres-server-debian-bookworm-17
+make -f Open-ILS/src/extras/Makefile.install postgres-server-debian-bullseye-17
+make -f Open-ILS/src/extras/Makefile.install postgres-server-debian-buster-17
+make -f Open-ILS/src/extras/Makefile.install postgres-server-ubuntu-jammy-17
+make -f Open-ILS/src/extras/Makefile.install postgres-server-ubuntu-noble-17
+------------------------------------------------------------------------------
+
+.Create the Evergreen PostgreSQL user
+
+You need to create a PostgreSQL superuser to create and access the database.
+Issue the following command as the *postgres* Linux account to create a new
+PostgreSQL superuser named `evergreen`. When prompted, enter the new user's
+password:
+
+[source, bash]
+------------------------------------------------------------------------------
+createuser -s -P evergreen
+------------------------------------------------------------------------------
+
+.Enabling connections to the PostgreSQL database
+
+Your PostgreSQL database may be configured by default to prevent connections,
+for example, it might reject attempts to connect via TCP/IP or from other
+servers. To enable TCP/IP connections from localhost, check your `pg_hba.conf`
+file, found in the `/etc/postgresql/` directory on Debian and Ubuntu.
+A simple way to enable TCP/IP
+connections from localhost to all databases with password authentication, which
+would be suitable for a test install of Evergreen on a single server, is to
+ensure the file contains the following entries _before_ any "host ... ident"
+entries:
+
+------------------------------------------------------------------------------
+host all all ::1/128 md5
+host all all 127.0.0.1/32 md5
+------------------------------------------------------------------------------
+
+When you change the `pg_hba.conf` file, you will need to reload PostgreSQL to
+make the changes take effect. For more information on configuring connectivity
+to PostgreSQL, see
+http://www.postgresql.org/docs/devel/static/auth-pg-hba-conf.html
+
+=== Creating the Evergreen database and schema ===
+
+Once you have created the *evergreen* PostgreSQL account, you also need to
+create the database and schema, and configure your configuration files to point
+at the database server. Issue the following command as the *root* Linux account
+from inside the Evergreen source directory, replacing <user>, <password>,
+<hostname>, <port>, and <dbname> with the appropriate values for your
+PostgreSQL database (where <user> and <password> are for the *evergreen*
+PostgreSQL account you just created), and replace <admin-user> and <admin-pass>
+with the values you want for the *egadmin* Evergreen administrator account:
+
+[source, bash]
+------------------------------------------------------------------------------
+perl Open-ILS/src/support-scripts/eg_db_config --update-config \
+ --service all --create-database --create-schema --create-offline \
+ --user <user> --password <password> --hostname <hostname> --port <port> \
+ --database <dbname> --admin-user <admin-user> --admin-pass <admin-pass>
+------------------------------------------------------------------------------
+
+This creates the database and schema and configures all of the services in
+your `/openils/conf/opensrf.xml` configuration file to point to that database.
+It also creates the configuration files required by the Evergreen `cgi-bin`
+administration scripts, and sets the user name and password for the *egadmin*
+Evergreen administrator account to your requested values.
+
+You can get a complete set of options for `eg_db_config` by passing the
+`--help` parameter.
+
+=== Loading sample data ===
+
+If you add the `--load-all-sample` parameter to the `eg_db_config` command,
+a set of authority and bibliographic records, call numbers, copies, staff
+and regular users, and transactions will be loaded into your target
+database. This sample dataset is commonly referred to as the _concerto_
+sample data, and can be useful for testing out Evergreen functionality and
+for creating problem reports that developers can easily recreate with their
+own copy of the _concerto_ sample data.
+
+If you don't mind waiting a little longer, you can install the _enhanced_
+concerto dataset. Use this flag: `--load-concerto-enhanced`. This includes
+all of the data from _concerto_. Notable differences include:
+
+. The organization units have friendly names
+. Acquisitions data
+. More billing scenarios
+. More shelving locations and shelving location settings
+. Authority data
+. Japanese, Spanish, French and Czech bib records
+. Metarecord holds
+. Item Stat Cats
+. Bookings data
+. Pre-created OPAC carousels
+. Serials data
+
+
+=== Creating the database on a remote server ===
+
+In a production instance of Evergreen, your PostgreSQL server should be
+installed on a dedicated server.
+
+To create the database instance on a remote database server, simply
+use the `--create-database` flag on `eg_db_config`.
+
+== Starting Evergreen ==
+
+1. As the *root* Linux account, start the `memcached` and `ejabberd` services
+(if they aren't already running):
++
+[source, bash]
+------------------------------------------------------------------------------
+/etc/init.d/ejabberd start
+/etc/init.d/memcached start
+------------------------------------------------------------------------------
++
+2. As the *opensrf* Linux account, start Evergreen. The `-l` flag in the
+following command is only necessary if you want to force Evergreen to treat the
+hostname as `localhost`; if you configured `opensrf.xml` using the real
+hostname of your machine as returned by `perl -ENet::Domain 'print
+Net::Domain::hostfqdn() . "\n";'`, you should not use the `-l` flag.
++
+[source, bash]
+------------------------------------------------------------------------------
+osrf_control -l --start-all
+------------------------------------------------------------------------------
++
+ ** If you receive the error message `bash: osrf_control: command not found`,
+ then your environment variable `PATH` does not include the `/openils/bin`
+ directory; this should have been set in the *opensrf* Linux account's
+ `.bashrc` configuration file. To manually set the `PATH` variable, edit the
+ configuration file `~/.bashrc` as the *opensrf* Linux account and add the
+ following line:
++
+[source, bash]
+------------------------------------------------------------------------------
+export PATH=$PATH:/openils/bin
+------------------------------------------------------------------------------
++
+3. As the *opensrf* Linux account, generate the Web files needed by the web staff
+ client and catalog and update the organization unit proximity (you need to do
+ this the first time you start Evergreen, and after that each time you change the library org unit configuration.
+):
++
+[source, bash]
+------------------------------------------------------------------------------
+autogen.sh
+------------------------------------------------------------------------------
++
+4. As the *root* Linux account, restart the Apache Web server:
++
+[source, bash]
+------------------------------------------------------------------------------
+/etc/init.d/apache2 restart
+------------------------------------------------------------------------------
++
+If the Apache Web server was running when you started the OpenSRF services, you
+might not be able to successfully log in to the OPAC or web staff client until the
+Apache Web server is restarted.
+
+== Testing connections to Evergreen ==
+
+Once you have installed and started Evergreen, test your connection to
+Evergreen via `srfsh`. As the *opensrf* Linux account, issue the following
+commands to start `srfsh` and try to log onto the Evergreen server using the
+*egadmin* Evergreen administrator user name and password that you set using the
+`eg_db_config` command:
+
+[source, bash]
+------------------------------------------------------------------------------
+/openils/bin/srfsh
+srfsh% login <admin-user> <admin-pass>
+------------------------------------------------------------------------------
+
+You should see a result like:
+
+ Received Data: "250bf1518c7527a03249858687714376"
+ ------------------------------------
+ Request Completed Successfully
+ Request Time in seconds: 0.045286
+ ------------------------------------
+
+ Received Data: {
+ "ilsevent":0,
+ "textcode":"SUCCESS",
+ "desc":" ",
+ "pid":21616,
+ "stacktrace":"oils_auth.c:304",
+ "payload":{
+ "authtoken":"e5f9827cc0f93b503a1cc66bee6bdd1a",
+ "authtime":420
+ }
+
+ }
+
+ ------------------------------------
+ Request Completed Successfully
+ Request Time in seconds: 1.336568
+ ------------------------------------
+[[install-troubleshooting-1]]
+If this does not work, it's time to do some troubleshooting.
+
+ * As the *opensrf* Linux account, run the `settings-tester.pl` script to see
+ if it finds any system configuration problems. The script is found at
+ `Open-ILS/src/support-scripts/settings-tester.pl` in the Evergreen source
+ tree.
+ * Follow the steps in the http://evergreen-ils.org/dokuwiki/doku.php?id=troubleshooting:checking_for_… guide].
+ * If you have faithfully followed the entire set of installation steps
+ listed here, you are probably extremely close to a working system.
+ Gather your configuration files and log files and contact the
+ http://evergreen-ils.org/communicate/mailing-lists/[Evergreen development
+mailing list] for assistance before making any drastic changes to your system
+ configuration.
+
+== Getting help ==
+
+Need help installing or using Evergreen? Join the mailing lists at
+http://evergreen-ils.org/communicate/mailing-lists/ or contact us on the Freenode
+IRC network on the #evergreen channel.
+
+== License ==
+
+This work is licensed under the Creative Commons Attribution-ShareAlike 3.0
+Unported License. To view a copy of this license, visit
+http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative
+Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA.
diff --git a/configure.ac b/configure.ac
index 71b8075f20..09d7f9f17b 100644
--- a/configure.ac
+++ b/configure.ac
@@ -20,7 +20,7 @@
export PATH=${PATH}:/usr/sbin
AC_PREREQ(2.61)
-AC_INIT(Open-ILS, trunk, open-ils-dev(a)list.georgialibraries.org)
+AC_INIT(Open-ILS, 3.15.0, open-ils-dev(a)list.georgialibraries.org)
AC_CONFIG_SRCDIR([configure.ac])
AC_CONFIG_MACRO_DIR([m4])
AM_INIT_AUTOMAKE
-----------------------------------------------------------------------
Summary of changes:
Open-ILS/src/perlmods/lib/OpenILS.pm | 2 +-
Open-ILS/src/perlmods/lib/OpenILS/Application.pm | 2 +-
Open-ILS/src/sql/Pg/002.schema.config.sql | 1 +
.../3.14.4-3.15.0-upgrade-db.sql} | 1242 +++++++++++++++++++-
README | 826 ++++++++++++-
configure.ac | 2 +-
6 files changed, 2070 insertions(+), 5 deletions(-)
copy Open-ILS/src/sql/Pg/{upgrade/1468.schema.openapi.sql => version-upgrade/3.14.4-3.15.0-upgrade-db.sql} (63%)
mode change 100755 => 100644
mode change 120000 => 100644 README
hooks/post-receive
--
Evergreen ILS
1
0