diff --git a/.gitignore b/.gitignore index 5be50c1..bb367ae 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ Manifest.toml /docs/build/ .vscode +.envrc +test/add_records.json \ No newline at end of file diff --git a/Project.toml b/Project.toml index fdb0bec..add8b8d 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Airtable" uuid = "96f7d883-6668-4fbe-bb01-b60427b16035" authors = ["Kevin Bonham PhD ", "contributors"] -version = "0.2.2" +version = "0.2.3" [deps] Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" diff --git a/docs/src/interface.md b/docs/src/interface.md index 833d34b..0098d10 100644 --- a/docs/src/interface.md +++ b/docs/src/interface.md @@ -105,7 +105,8 @@ AirRecord("recYvPIayZx1okJ41", AirTable("Table 1"), (Name = "Some Record", Notes Notice that the return value is an [`AirRecord`](@ref). It can be useful to hold onto this, since it contains the unique identifier. -You can also pass a vector of `NamedTuple`s to create multiple records. +You can also pass a vector of `NamedTuple`s to create multiple records +(`post!` will automatically limit 10 records at a time, in compliance with the Airtable API). See the [note about rate limits](@ref ratelimit). @@ -128,6 +129,10 @@ julia> Airtable.patch!(new_rec, (; Status="Done", Notes=missing)) AirRecord("recYvPIayZx1okJ41", AirTable("Table 1"), (Name = "Some Record", Status = "Done")) ``` +You can also pass a vector of `AirRecords` +along with an equal-length vector of `NamedTuple`s to patch multiple records at once +(`patch!` will automatically limit to 10 records at a time, in compliance with the Airtable API). + ### Using `delete!` To remove a record, simply pass an `AirRecord` with the same `id` to [`delete!`](@ref). @@ -142,9 +147,6 @@ JSON3.Object{Base.CodeUnits{UInt8, String}, Vector{UInt64}} with 2 entries: [^1]: This is the default, you can change this with the `pageSize` parameter, but 100 is the maximum. -### Add/update records - - ## [A note on rate limits](@id ratelimit) Airtable.com only allows 5 requests / sec. diff --git a/src/interface.jl b/src/interface.jl index 777859e..a0925d1 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -208,10 +208,13 @@ end function post!(cred::Credential, tab::AirTable, recs::Vector{<:NamedTuple}) - recs = (;records = [(; fields = nt) for nt in recs]) - resp = post!(cred, path(tab), ["Content-Type" => "application/json"], JSON3.write(recs)) - - return _extract_records(tab, resp) + resps = AirRecord[] + for recpart in Iterators.partition(recs, 10) + topost = (; records = [(; fields = nt) for nt in recpart]) + resp = post!(cred, path(tab), ["Content-Type" => "application/json"], JSON3.write(topost)) + append!(resps, _extract_records(tab, resp)) + end + return resps end post!(tab::AirTable, rec::AirRecord) = post!(Credential(), tab, rec) @@ -223,9 +226,30 @@ delete!(cred::Credential, rec::AirRecord) = delete!(cred, path(rec)) delete!(rec::AirRecord) = delete!(Credential(), rec) patch!(cred::Credential, rec::AirRecord) = _extract_record(table(rec), patch!(cred, path(rec), ["Content-Type" => "application/json"], JSON3.write(rec))) +patch!(cred::Credential, rec::AirRecord, fields::NamedTuple) = _extract_record(table(rec), patch!(cred, path(rec), ["Content-Type" => "application/json"], JSON3.write((; fields)))) + +function patch!(cred::Credential, tab::AirTable, recs::Vector{<:AirRecord}) + resps = AirRecord[] + for recpart in Iterators.partition(recs, 10) + resp = patch!(cred, path(tab), ["Content-Type" => "application/json"], JSON3.write((; records = [ + (; id=id(rec), fields=fields(rec)) for rec in recpart + ]))) + append!(resps, _extract_records(tab, resp)) + end + return resps +end + +function patch!(cred::Credential, tab::AirTable, recs::Vector{<:AirRecord}, fields::Vector{<:NamedTuple}) + length(recs) == length(fields) || throw(ArgumentError("Lengths of records vector and fields vector must be the same")) + patch!(cred, tab, [AirRecord(Airtable.id(rec), tab, fs) for (rec, fs) in zip(recs, fields)]) +end + + patch!(rec::AirRecord) = patch!(Credential(), rec) -patch!(cred::Credential, rec::AirRecord, fields::NamedTuple) = _extract_record(table(rec), patch!(cred, path(rec), ["Content-Type" => "application/json"], string("""{ "fields": """, JSON3.write(fields), " }"))) patch!(rec::AirRecord, fields::NamedTuple) = patch!(Credential(), rec, fields) +patch!(tab::AirTable, recs::Vector{<:AirRecord}) = patch!(Credential(), tab, recs) +patch!(tab::AirTable, recs::Vector{<:AirRecord}, fields::Vector{<:NamedTuple}) = patch!(Credential(), tab, recs, fields) + Base.getindex(rec::AirRecord, k::Symbol) = fields(rec)[k] Base.keys(rec::AirRecord) = keys(fields(rec)) diff --git a/test/AirtableTests.jl b/test/AirtableTests.jl index 2fe6434..a0bed0a 100644 --- a/test/AirtableTests.jl +++ b/test/AirtableTests.jl @@ -4,6 +4,10 @@ using Airtable using Airtable.HTTP using Airtable.JSON3 using ReTest +using Random + +const CI_STRING = randstring() + @testset "Airtable.jl" begin @testset "Constructors" begin @@ -26,8 +30,18 @@ using ReTest @test length(Airtable.query(tab; filterByFormula="{Keep}")) == 3 - resp = Airtable.post!(tab, open(joinpath(@__DIR__, "add_records.json"))) - + toadd = [ + (; Name = "TEST1", + Notes = "Some note", + Status = "Todo", + CI = CI_STRING), + (; Name = "TEST2", + Notes = "Other note", + Status = "Done", + CI = CI_STRING) + ] + + resp = Airtable.post!(tab, toadd) @test resp isa Vector{AirRecord} @test length(resp) == 2 @@ -35,18 +49,26 @@ using ReTest sleep(1) # avoid over-taxing api @test rec[:Status] != "In progress" - @test Airtable.patch!(rec, (; Status="In progress")).id == Airtable.id(rec) + @test Airtable.patch!(rec, (; Status="In progress", CI=CI_STRING)).id == Airtable.id(rec) @test Airtable.get(rec)[:Status] == "In progress" + @test_throws HTTP.ExceptionRequest.StatusError Airtable.patch!(rec, (; Status="Not valid")) Airtable.patch!(rec, (; Status = rec[:Status])) - @test keys(rec) == (:Name, :Notes, :Status) + @test keys(rec) == (:Name, :Notes, :Status, :CI) @test Airtable.delete!(rec).deleted @test_throws HTTP.ExceptionRequest.StatusError Airtable.get(rec) end + + JSON3.write("add_records.json", (; records = [(; fields = rec) for rec in toadd])) + resp = Airtable.post!(tab, open("add_records.json")) + + Airtable.patch!(tab, resp, [(; Status="In progress", CI=CI_STRING) for _ in 1:length(resp)]) + @test all([Airtable.get(rec)[:Status] == "In progress" for rec in resp]) + end # Cleanup - dontkeep = Airtable.query(AirTable("Table 1", AirBase("appphImnhJO8AXmmo")); filterByFormula="NOT({Keep})") + dontkeep = Airtable.query(AirTable("Table 1", AirBase("appphImnhJO8AXmmo")); filterByFormula="AND({CI}='$CI_STRING', NOT({Keep}))") if !isempty(dontkeep) sleep(1) for rec in dontkeep diff --git a/test/Manifest.toml b/test/Manifest.toml deleted file mode 100644 index f9634d5..0000000 --- a/test/Manifest.toml +++ /dev/null @@ -1,57 +0,0 @@ -# This file is machine-generated - editing it directly is not advised - -manifest_format = "2.0" - -[[deps.Base64]] -uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" - -[[deps.Distributed]] -deps = ["Random", "Serialization", "Sockets"] -uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" - -[[deps.InlineTest]] -deps = ["Test"] -git-tree-sha1 = "daf0743879904f0ad645ca6594e1479685f158a2" -uuid = "bd334432-b1e7-49c7-a2dc-dd9149e4ebd6" -version = "0.2.0" - -[[deps.InteractiveUtils]] -deps = ["Markdown"] -uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" - -[[deps.Logging]] -uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" - -[[deps.Markdown]] -deps = ["Base64"] -uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" - -[[deps.Printf]] -deps = ["Unicode"] -uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" - -[[deps.Random]] -deps = ["SHA", "Serialization"] -uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" - -[[deps.ReTest]] -deps = ["Distributed", "InlineTest", "Printf", "Random", "Sockets", "Test"] -git-tree-sha1 = "dd8f6587c0abac44bcec2e42f0aeddb73550c0ec" -uuid = "e0db7c4e-2690-44b9-bad6-7687da720f89" -version = "0.3.2" - -[[deps.SHA]] -uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" - -[[deps.Serialization]] -uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" - -[[deps.Sockets]] -uuid = "6462fe0b-24de-5631-8697-dd941f90decc" - -[[deps.Test]] -deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] -uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" - -[[deps.Unicode]] -uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" diff --git a/test/Project.toml b/test/Project.toml index 99566ce..f41c931 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,3 +1,4 @@ [deps] +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" ReTest = "e0db7c4e-2690-44b9-bad6-7687da720f89" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/add_records.json b/test/add_records.json deleted file mode 100644 index 0d4946e..0000000 --- a/test/add_records.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "records": [ - { - "fields": { - "Name": "TEST1", - "Notes": "Some note", - "Status": "Todo" - } - }, - { - "fields": { - "Name": "TEST2", - "Notes": "Other note", - "Status": "Done" - } - } - ] - } \ No newline at end of file