feat: add JEI_payment_batches schema migration script
This commit is contained in:
parent
9e0ec2c6a5
commit
3290803e0b
1 changed files with 198 additions and 0 deletions
198
scripts/add_payment_batch_field.py
Normal file
198
scripts/add_payment_batch_field.py
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
"""
|
||||
Add the JEI_payment_batches collection and a payment_batch relation on JEI_invoices.
|
||||
|
||||
Idempotent: if the collection or the relation field already exists, the script
|
||||
reports EXISTS and reuses the existing id.
|
||||
|
||||
PB v0.39.8 API quirks (verified 2026-09-17, recorded in NOTES.md):
|
||||
1. Field options for select/relation go at the FIELD ROOT, not inside `options`.
|
||||
2. POST /api/collections uses key `fields`, not `schema`.
|
||||
3. GET hides field options for select/relation (data IS stored, just not echoed back).
|
||||
4. PATCH round-trip "wakes up" newly created collections. If a collection 403s on
|
||||
read with empty listRule, PATCH any rule field (even with the same value) once.
|
||||
|
||||
Run: python scripts/add_payment_batch_field.py
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import urllib.request
|
||||
from urllib.error import HTTPError
|
||||
|
||||
PB_URL = os.environ.get("PB_URL", "https://pb.agent.apphub.co.za")
|
||||
PB_TOKEN = os.environ.get("PB_TOKEN", "")
|
||||
if not PB_TOKEN:
|
||||
env_path = os.path.join(os.path.dirname(__file__), "..", ".env.local")
|
||||
if os.path.exists(env_path):
|
||||
with open(env_path, "r", encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
if line.startswith("PB_TOKEN="):
|
||||
PB_TOKEN = line.strip().split("=", 1)[1]
|
||||
break
|
||||
if not PB_TOKEN:
|
||||
sys.exit("ERROR: PB_TOKEN not set")
|
||||
|
||||
HEADERS = {"Authorization": PB_TOKEN, "Content-Type": "application/json"}
|
||||
|
||||
|
||||
def pb_get(path):
|
||||
req = urllib.request.Request(PB_URL + path, headers=HEADERS, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
|
||||
def pb_post(path, body):
|
||||
req = urllib.request.Request(
|
||||
PB_URL + path,
|
||||
data=json.dumps(body).encode("utf-8"),
|
||||
headers=HEADERS,
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
return json.loads(r.read()), None
|
||||
except HTTPError as e:
|
||||
return None, json.loads(e.read())
|
||||
|
||||
|
||||
def pb_patch(path, body):
|
||||
req = urllib.request.Request(
|
||||
PB_URL + path,
|
||||
data=json.dumps(body).encode("utf-8"),
|
||||
headers=HEADERS,
|
||||
method="PATCH",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
return json.loads(r.read()), None
|
||||
except HTTPError as e:
|
||||
return None, json.loads(e.read())
|
||||
|
||||
|
||||
SUPERUSER_RULES = {
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": "",
|
||||
"updateRule": "",
|
||||
"deleteRule": "",
|
||||
}
|
||||
|
||||
|
||||
def payment_batches_schema():
|
||||
return {
|
||||
"name": "JEI_payment_batches",
|
||||
"type": "base",
|
||||
"fields": [
|
||||
{
|
||||
"name": "batch_ref",
|
||||
"type": "text",
|
||||
"required": True,
|
||||
"min": 1,
|
||||
"max": 50,
|
||||
"pattern": "^[A-Za-z0-9_-]+$",
|
||||
},
|
||||
{
|
||||
"name": "payment_date",
|
||||
"type": "date",
|
||||
"required": True,
|
||||
"min": "",
|
||||
"max": "",
|
||||
},
|
||||
{
|
||||
"name": "pop_file",
|
||||
"type": "file",
|
||||
"required": True,
|
||||
"maxSelect": 1,
|
||||
"maxSize": 8388608,
|
||||
"mimeTypes": ["application/pdf", "image/jpeg", "image/png"],
|
||||
"thumbs": [],
|
||||
},
|
||||
{
|
||||
"name": "notes",
|
||||
"type": "text",
|
||||
"required": False,
|
||||
"min": 0,
|
||||
"max": 1000,
|
||||
"pattern": "",
|
||||
},
|
||||
],
|
||||
**SUPERUSER_RULES,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
print(f"PB_URL: {PB_URL}")
|
||||
|
||||
# 1. Find or create JEI_payment_batches
|
||||
listing = pb_get("/api/collections?perPage=200")
|
||||
batches_id = None
|
||||
for c in listing.get("items", []):
|
||||
if c.get("name") == "JEI_payment_batches":
|
||||
batches_id = c.get("id")
|
||||
break
|
||||
|
||||
if batches_id:
|
||||
print(f"EXISTS: JEI_payment_batches (id={batches_id})")
|
||||
else:
|
||||
resp, err = pb_post("/api/collections", payment_batches_schema())
|
||||
if err is not None:
|
||||
sys.exit(f"ERROR: failed to create JEI_payment_batches: {err}")
|
||||
batches_id = resp.get("id")
|
||||
print(f"CREATED: JEI_payment_batches (id={batches_id})")
|
||||
|
||||
# 2. Find JEI_invoices
|
||||
invoices_id = None
|
||||
for c in listing.get("items", []):
|
||||
if c.get("name") == "JEI_invoices":
|
||||
invoices_id = c.get("id")
|
||||
break
|
||||
if not invoices_id:
|
||||
sys.exit("ERROR: JEI_invoices collection not found")
|
||||
|
||||
print(f"FOUND: JEI_invoices (id={invoices_id})")
|
||||
|
||||
# 3. Check whether the relation field already exists
|
||||
inv = pb_get(f"/api/collections/{invoices_id}")
|
||||
field_names = {f.get("name") for f in inv.get("fields", [])}
|
||||
|
||||
if "payment_batch" in field_names:
|
||||
print("EXISTS: JEI_invoices.payment_batch field")
|
||||
else:
|
||||
new_field = {
|
||||
"name": "payment_batch",
|
||||
"type": "relation",
|
||||
"required": False,
|
||||
"collectionId": batches_id,
|
||||
"cascadeDelete": False,
|
||||
"minSelect": 0,
|
||||
"maxSelect": 1,
|
||||
}
|
||||
# PATCH replaces the full fields array, so send the existing fields
|
||||
# plus the new one appended.
|
||||
patched_fields = list(inv.get("fields", [])) + [new_field]
|
||||
# Strip PB-echoed-only keys that should not be re-sent verbatim.
|
||||
clean_fields = []
|
||||
for f in patched_fields:
|
||||
clean_fields.append({k: v for k, v in f.items() if k != "options"})
|
||||
resp, err = pb_patch(
|
||||
f"/api/collections/{invoices_id}",
|
||||
{"fields": clean_fields},
|
||||
)
|
||||
if err is not None:
|
||||
sys.exit(f"ERROR: failed to add payment_batch field: {err}")
|
||||
print(f"ADDED: JEI_invoices.payment_batch relation -> {batches_id}")
|
||||
|
||||
# 4. Wake-up round-trip: PATCH each collection's rule fields. Even though
|
||||
# the values are already empty strings, the round-trip activates the
|
||||
# collection's rule machinery in PB v0.39.8.
|
||||
for cid in (batches_id, invoices_id):
|
||||
_, err = pb_patch(f"/api/collections/{cid}", dict(SUPERUSER_RULES))
|
||||
if err is not None:
|
||||
sys.exit(f"ERROR: wake-up PATCH failed for {cid}: {err}")
|
||||
print(f"PATCHED: {cid} (wake-up round-trip)")
|
||||
|
||||
print("DONE: schema ready.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in a new issue