Refactor to make this more obviously HFF's Notion API client.
This commit is contained in:
47
src/hff_notion_api_client/config.nim
Normal file
47
src/hff_notion_api_client/config.nim
Normal file
@ -0,0 +1,47 @@
|
||||
import std/json, std/sequtils, std/times
|
||||
import timeutils
|
||||
|
||||
type
|
||||
HffNotionConfigProperty = tuple[key: string, textNode: JsonNode, dateNode: JsonNode]
|
||||
|
||||
HffNotionConfig* = object
|
||||
addressesDbId*: string
|
||||
familiesDbId*: string
|
||||
peopleDbId*: string
|
||||
syncRecordsDbId*: string
|
||||
lastSyncedAt*: DateTime
|
||||
|
||||
proc getCfgProp(cfgPages: JsonNode, key: string): HffNotionConfigProperty =
|
||||
|
||||
let propNodes = cfgPages.filterIt(
|
||||
it{"properties", "Key", "title"}[0]["plain_text"].getStr == key)
|
||||
|
||||
if propNodes.len != 1:
|
||||
raise newException(Exception,
|
||||
"there is no configuration value for key '" & key &
|
||||
"' in the Notion Configuration table.")
|
||||
|
||||
return (
|
||||
propNodes[0]{"properties", "Key", "title"}[0]["plain_text"].getStr,
|
||||
propNodes[0]{"properties", "TextValue", "rich_text"},
|
||||
propNodes[0]{"properties", "DateTimeValue", "date"})
|
||||
|
||||
proc dateValue(prop: HffNotionConfigProperty): DateTime =
|
||||
if isNil(prop.dateNode) or prop.dateNode.kind == JNull:
|
||||
raise newException(Exception, prop.key & " does not have a date value")
|
||||
|
||||
return parseIso8601(prop.dateNode["start"].getStr)
|
||||
|
||||
proc textValue(prop: HffNotionConfigProperty): string =
|
||||
if prop.textNode.len == 0:
|
||||
raise newException(Exception, prop.key & " does not have a text value")
|
||||
|
||||
return prop.textNode[0]["plain_text"].getStr
|
||||
|
||||
proc parseHffNotionConfig*(n: JsonNode): HffNotionConfig =
|
||||
result = HffNotionConfig(
|
||||
addressesDbId: n.getCfgProp("addressesDbId").textValue,
|
||||
familiesDbId: n.getCfgProp("familiesDbId").textValue,
|
||||
peopleDbId: n.getCfgProp("peopleDbId").textValue,
|
||||
syncRecordsDbId: n.getCfgProp("syncRecordsDbId").textValue,
|
||||
lastSyncedAt: n.getCfgProp("lastSyncedAt").dateValue)
|
223
src/hff_notion_api_client/models.nim
Normal file
223
src/hff_notion_api_client/models.nim
Normal file
@ -0,0 +1,223 @@
|
||||
import std/json, std/options, std/sequtils, std/sets, std/strutils, std/tables,
|
||||
std/times
|
||||
|
||||
import timeutils
|
||||
|
||||
import ./utils
|
||||
|
||||
type
|
||||
AddressObj* = object
|
||||
id*: string
|
||||
city*: string
|
||||
state*: string
|
||||
street*: string
|
||||
zipCode*: string
|
||||
createdAt*: Option[DateTime]
|
||||
lastUpdatedAt*: Option[DateTime]
|
||||
|
||||
|
||||
FamilyObj* = object
|
||||
id*: string
|
||||
name*: string
|
||||
headOfHouseholdIds*: seq[string]
|
||||
primaryAddressId*: seq[string]
|
||||
memberIds*: seq[string]
|
||||
createdAt*: Option[DateTime]
|
||||
lastUpdatedAt*: Option[DateTime]
|
||||
|
||||
PersonObj* = object
|
||||
id*: string
|
||||
preferredName*: string
|
||||
firstName*: string
|
||||
middleNames*: string
|
||||
lastName*: string
|
||||
birthDate*: Option[DateTime]
|
||||
gender*: string
|
||||
primaryPhoneNumber*: string
|
||||
primaryEmailAddress*: string
|
||||
addressIds*: seq[string]
|
||||
marriedToId*: seq[string]
|
||||
anniversary*: Option[DateTime]
|
||||
parentIds*: seq[string]
|
||||
childIds*: seq[string]
|
||||
relationshipToHff*: seq[string]
|
||||
createdAt*: Option[DateTime]
|
||||
lastUpdatedAt*: Option[DateTime]
|
||||
apiPermissions*: seq[string]
|
||||
|
||||
RecordType* = enum rtPerson, rtFamily, rtAddress
|
||||
|
||||
SyncRecordObj* = object
|
||||
id*: string
|
||||
knownIds*: HashSet[string]
|
||||
recType*: RecordType
|
||||
notionId*: string
|
||||
pcoId*: string
|
||||
|
||||
|
||||
Address* = ref AddressObj
|
||||
Family* = ref FamilyObj
|
||||
Person* = ref PersonObj
|
||||
SyncRecord* = ref SyncRecordObj
|
||||
|
||||
MembershipDataSet* = ref object
|
||||
addresses*: TableRef[string, Address]
|
||||
families*: TableRef[string, Family]
|
||||
people*: TableRef[string, Person]
|
||||
|
||||
func sameContents[T](a: seq[T], b: seq[T]): bool =
|
||||
let aCount = toCountTable(a)
|
||||
let bCount = toCountTable(b)
|
||||
|
||||
for k in aCount.keys:
|
||||
if not bCount.hasKey(k) or aCount[k] != bCount[k]: return false
|
||||
|
||||
return aCount.len == bCount.len
|
||||
|
||||
|
||||
func `==`*(a, b: Family): bool =
|
||||
return a.name == b.name and
|
||||
sameContents(a.headOfHouseholdIds, b.headOfHouseholdIds) and
|
||||
sameContents(a.primaryAddressId, b.primaryAddressId) and
|
||||
sameContents(a.memberIds, b.memberIds)
|
||||
|
||||
func `==`*(a, b: Person): bool =
|
||||
return a.preferredName == b.preferredName and
|
||||
a.firstName == b.firstName and
|
||||
a.middleNames == b.middleNames and
|
||||
a.lastName == b.lastName and
|
||||
a.birthDate == b.birthDate and
|
||||
a.gender == b.gender and
|
||||
a.primaryPhoneNumber == b.primaryPhoneNumber and
|
||||
a.primaryEmailAddress == b.primaryEmailAddress and
|
||||
a.anniversary == b.anniversary and
|
||||
sameContents(a.addressIds, b.addressIds) and
|
||||
sameContents(a.marriedToId, b.marriedToId) and
|
||||
sameContents(a.parentIds, b.parentIds) and
|
||||
sameContents(a.childIds, b.childIds) and
|
||||
sameContents(a.relationshipToHff, b.relationshipToHff)
|
||||
|
||||
func `$`*(rt: RecordType): string =
|
||||
case rt:
|
||||
of rtPerson: "Person"
|
||||
of rtFamily: "Family"
|
||||
of rtAddress: "Address"
|
||||
|
||||
func initSyncRecord*(recType: RecordType, notionId, pcoId: string, knownIds = initHashSet[string]()): SyncRecord =
|
||||
result = SyncRecord(
|
||||
recType: recType,
|
||||
notionId: notionId,
|
||||
pcoId: pcoId,
|
||||
knownIds: knownIds)
|
||||
|
||||
result.knownIds.incl(notionId)
|
||||
result.knownIds.incl(pcoId)
|
||||
|
||||
|
||||
func toPage*(a: Address): JsonNode =
|
||||
%*{
|
||||
"properties": {
|
||||
"City": makeTextProp("rich_text", a.city),
|
||||
"State": makeSelectProp(a.state),
|
||||
"Street Address": makeTextProp("title", a.street),
|
||||
"Zip Code": makeTextProp("rich_text", a.zipCode)
|
||||
}
|
||||
}
|
||||
|
||||
func toPage*(f: Family): JsonNode =
|
||||
%*{
|
||||
"properties": {
|
||||
"Name": makeTextProp("title", f.name),
|
||||
"Head(s) of Household": makeRelationProp(f.headOfHouseholdIds),
|
||||
"Primary Address": makeRelationProp(f.primaryAddressId),
|
||||
"Members": makeRelationProp(f.memberIds)
|
||||
}
|
||||
}
|
||||
|
||||
func toPage*(p: Person): JsonNode =
|
||||
%*{
|
||||
"properties": {
|
||||
"Preferred Name": makeTextProp("title", p.preferredName),
|
||||
"First Name": makeTextProp("rich_text", p.firstName),
|
||||
"Middle Names": makeTextProp("rich_text", p.middleNames),
|
||||
"Last Name": makeTextProp("rich_text", p.lastName),
|
||||
"Birth Date": makeDateProp(p.birthDate),
|
||||
"Gender": makeSelectProp(p.gender),
|
||||
"Primary Phone Number": { "phone_number": p.primaryPhoneNumber },
|
||||
"Email Address": { "email": p.primaryEmailAddress },
|
||||
"Relationship to HFF": makeMultiSelectProp(p.relationshipToHff),
|
||||
"Address": makeRelationProp(p.addressIds),
|
||||
"Married To": makeRelationProp(p.marriedToId),
|
||||
"Anniversary": makeDateProp(p.anniversary),
|
||||
"Parents": makeRelationProp(p.parentIds),
|
||||
"Children": makeRelationProp(p.childIds),
|
||||
"API Permissions": makeMultiSelectProp(p.apiPermissions),
|
||||
}
|
||||
}
|
||||
|
||||
func toPage*(sr: SyncRecord): JsonNode =
|
||||
result = %*{
|
||||
"properties": {
|
||||
"Notion Rec Id": makeTextProp("title", sr.notionId),
|
||||
"PCO Rec Id": makeTextProp("rich_text", sr.pcoId),
|
||||
"Alternate IDs": makeTextProp("rich_text", toSeq(sr.knownIds).join(";"))
|
||||
}
|
||||
}
|
||||
|
||||
case sr.recType
|
||||
of rtAddress: result{"properties", "Rec Type"}= %"Address"
|
||||
of rtFamily: result{"properties", "Rec Type"}= %"Family"
|
||||
of rtPerson: result{"properties", "Rec Type"}= %"Person"
|
||||
|
||||
proc addressFromPage*(page: JsonNode): Address =
|
||||
result = Address(
|
||||
id: page["id"].getStr,
|
||||
city: page.getText("City"),
|
||||
state: page.getSelect("State"),
|
||||
street: page.getTitle("Street Address"),
|
||||
zipCode: page.getText("Zip Code"),
|
||||
createdAt: some(parseIso8601(page["created_time"].getStr)),
|
||||
lastUpdatedAt: some(parseIso8601(page["last_edited_time"].getStr)))
|
||||
|
||||
proc familyFromPage*(page: JsonNode): Family =
|
||||
result = Family(
|
||||
id: page["id"].getStr,
|
||||
name: page.getTitle("Name"),
|
||||
headOfHouseholdIds: page.getRelationIds("Head(s) of Household"),
|
||||
primaryAddressId: page.getRelationIds("Primary Address"),
|
||||
memberIds: page.getRelationIds("Members"),
|
||||
createdAt: some(parseIso8601(page["created_time"].getStr)),
|
||||
lastUpdatedAt: some(parseIso8601(page["last_edited_time"].getStr)))
|
||||
|
||||
proc personFromPage*(page: JsonNode): Person =
|
||||
result = Person(
|
||||
id: page["id"].getStr,
|
||||
preferredName: page.getTitle("Preferred Name"),
|
||||
firstName: page.getText("First Name"),
|
||||
middleNames: page.getText("Middle Names"),
|
||||
lastName: page.getText("Last Name"),
|
||||
birthDate: page.getDateTime("Birth Date"),
|
||||
gender: page.getSelect("Gender"),
|
||||
primaryPhoneNumber: page.getPhone("Primary Phone Number"),
|
||||
primaryEmailAddress: page.getEmail("Email Address"),
|
||||
relationshipToHff: page.getMultiSelect("Relationship to HFF"),
|
||||
addressIds: page.getRelationIds("Address"),
|
||||
marriedToId: page.getRelationIds("Married To"),
|
||||
anniversary: page.getDateTime("Anniversary"),
|
||||
parentIds: page.getRelationIds("Parents"),
|
||||
childIds: page.getRelationIds("Children"),
|
||||
createdAt: some(parseIso8601(page["created_time"].getStr)),
|
||||
lastUpdatedAt: some(parseIso8601(page["last_edited_time"].getStr)),
|
||||
apiPermissions: page.getMultiSelect("API Permissions"))
|
||||
|
||||
func syncRecordFromPage*(page: JsonNode): SyncRecord =
|
||||
result = SyncRecord(
|
||||
id: page["id"].getStr,
|
||||
notionId: page.getTitle("Notion Rec Id"),
|
||||
pcoId: page.getText("PCO Rec Id"),
|
||||
knownIds: toHashSet(page.getText("Alternate IDs").split(";")))
|
||||
|
||||
case page.getSelect("Record Type")
|
||||
of "Address": result.recType = rtAddress
|
||||
of "Family": result.recType = rtFamily
|
||||
of "Person": result.recType = rtPerson
|
20
src/hff_notion_api_client/sequtils_ext.nim
Normal file
20
src/hff_notion_api_client/sequtils_ext.nim
Normal file
@ -0,0 +1,20 @@
|
||||
import std/options, std/sugar, std/tables
|
||||
|
||||
func sameContents*[T](a: seq[T], b: seq[T]): bool =
|
||||
let aCount = toCountTable(a)
|
||||
let bCount = toCountTable(b)
|
||||
|
||||
for k in aCount.keys:
|
||||
if not bCount.hasKey(k) or aCount[k] != bCount[k]: return false
|
||||
|
||||
return aCount.len == bCount.len
|
||||
|
||||
func findBy*[T](s: seq[T], operation: (T) -> bool): Option[T] =
|
||||
for i in s:
|
||||
if operation(i):
|
||||
return some(i)
|
||||
return none[T]()
|
||||
|
||||
proc mapById*[T](records: seq[T]): TableRef[string, T] =
|
||||
result = newTable[string, T]()
|
||||
for r in records: result[r.id] = r
|
95
src/hff_notion_api_client/utils.nim
Normal file
95
src/hff_notion_api_client/utils.nim
Normal file
@ -0,0 +1,95 @@
|
||||
import std/json, std/options, std/sequtils, std/times
|
||||
import timeutils
|
||||
|
||||
const NOTION_MAX_PAGE_SIZE* = 100
|
||||
const NOTION_DATE_FORMAT = "YYYY-MM-dd"
|
||||
|
||||
proc parseDate(str: string): DateTime =
|
||||
try: result = parseIso8601(str)
|
||||
except: result = times.parse(str, NOTION_DATE_FORMAT)
|
||||
|
||||
## Utility functions for creating Page property values
|
||||
## ---------------------------------------------------
|
||||
|
||||
proc makeDateProp*(d: Option[DateTime]): JsonNode =
|
||||
if d.isSome: return %*{ "date": { "start": format(d.get, NOTION_DATE_FORMAT) } }
|
||||
else: return %*{ "date": nil }
|
||||
|
||||
proc makeDateTimeProp*(d: Option[DateTime]): JsonNode =
|
||||
if d.isSome: return %*{ "date": { "start": formatIso8601(d.get) } }
|
||||
else: return %*{ "date": nil }
|
||||
|
||||
proc makeIntervalProp*(s: Option[DateTime], e: Option[DateTime]): JsonNode =
|
||||
if not s.isSome: result = %*{ "date": nil }
|
||||
|
||||
result = %*{ "date": { "start": formatIso8601(s.get) } }
|
||||
|
||||
if e.isSome: result["date"]["end"] = %formatIso8601(e.get)
|
||||
|
||||
proc makeMultiSelectProp*(values: seq[string]): JsonNode =
|
||||
if values.len == 0: return %*{ "multi_select": [] }
|
||||
return %*{ "multi_select": values.mapIt(%*{ "name": it }) }
|
||||
|
||||
proc makeRelationProp*(ids: seq[string]): JsonNode =
|
||||
if ids.len == 0: return %*{ "relation": [] }
|
||||
return %*{ "relation": ids.mapIt(%*{ "id": it }) }
|
||||
|
||||
proc makeSelectProp*(value: string): JsonNode =
|
||||
return %*{ "select": { "name": value } }
|
||||
|
||||
proc makeTextProp*(propType: string, value: string): JsonNode =
|
||||
return %*{
|
||||
propType: [
|
||||
{
|
||||
"type": "text",
|
||||
"text": { "content": value }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
## Utility functions for reading Page property values
|
||||
## --------------------------------------------------
|
||||
|
||||
proc getPropNode(page: JsonNode, propType, propName: string): JsonNode =
|
||||
result = page{"properties", propName, propType}
|
||||
|
||||
if isNil(result):
|
||||
raise newException(ValueError,
|
||||
"could not find a " & propType & " property named '" & propName &
|
||||
"' in the Notion page (id: " & page["id"].getStr & ")")
|
||||
|
||||
proc getEmail*(page: JsonNode, propName: string): string =
|
||||
let propNode = page.getPropNode("email", propName)
|
||||
return propNode.getStr
|
||||
|
||||
proc getMultiSelect*(page: JsonNode, propName: string): seq[string] =
|
||||
let propNode = page.getPropNode("multi_select", propName)
|
||||
return propNode.getElems.mapIt(it["name"].getStr)
|
||||
|
||||
proc getPhone*(page: JsonNode, propName: string): string =
|
||||
let propNode = page.getPropNode("phone_number", propName)
|
||||
return propNode.getStr
|
||||
|
||||
proc getRelationIds*(page: JsonNode, propName: string): seq[string] =
|
||||
let propNode = page.getPropNode("relation", propName)
|
||||
return propNode.getElems.mapIt(it["id"].getStr)
|
||||
|
||||
proc getText*(page: JsonNode, propName: string): string =
|
||||
let propNode = page.getPropNode("rich_text", propName)
|
||||
if propNode.len == 0: return ""
|
||||
return propNode[0]["plain_text"].getStr
|
||||
|
||||
proc getTitle*(page: JsonNode, propName: string): string =
|
||||
let propNode = page.getPropNode("title", propName)
|
||||
if propNode.len == 0: return ""
|
||||
return propNode[0]["plain_text"].getStr
|
||||
|
||||
proc getSelect*(page: JsonNode, propName: string): string =
|
||||
let propNode = page.getPropNode("select", propName)
|
||||
if propNode.kind == JNull: return ""
|
||||
return propNode["name"].getStr
|
||||
|
||||
proc getDateTime*(page: JsonNode, propName: string): Option[DateTime] =
|
||||
let propNode = page.getPropNode("date", propName)
|
||||
if propNode.kind == JNull: result = none[DateTime]()
|
||||
else: result = some(parseDate(propNode["start"].getStr))
|
Reference in New Issue
Block a user