sequel-notion 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +207 -0
- data/LICENSE.txt +21 -0
- data/README.md +470 -0
- data/lib/sequel/adapters/notion.rb +159 -0
- data/lib/sequel/notion/dataset.rb +167 -0
- data/lib/sequel/notion/dataset_aggregates.rb +82 -0
- data/lib/sequel/notion/dataset_compounds.rb +89 -0
- data/lib/sequel/notion/dataset_computed.rb +124 -0
- data/lib/sequel/notion/dataset_grouping.rb +145 -0
- data/lib/sequel/notion/dataset_having.rb +99 -0
- data/lib/sequel/notion/dataset_joins.rb +124 -0
- data/lib/sequel/notion/dataset_pages.rb +145 -0
- data/lib/sequel/notion/dataset_selection.rb +51 -0
- data/lib/sequel/notion/dataset_truncation.rb +39 -0
- data/lib/sequel/notion/discovery.rb +76 -0
- data/lib/sequel/notion/errors.rb +9 -0
- data/lib/sequel/notion/filter_comparison.rb +123 -0
- data/lib/sequel/notion/filter_compiler.rb +121 -0
- data/lib/sequel/notion/filter_constants.rb +37 -0
- data/lib/sequel/notion/filter_helpers.rb +162 -0
- data/lib/sequel/notion/filter_like.rb +143 -0
- data/lib/sequel/notion/filter_like_tokenizer.rb +42 -0
- data/lib/sequel/notion/filter_negation.rb +76 -0
- data/lib/sequel/notion/filter_nested.rb +96 -0
- data/lib/sequel/notion/filter_nulls.rb +53 -0
- data/lib/sequel/notion/filter_predicates.rb +160 -0
- data/lib/sequel/notion/filter_shape.rb +75 -0
- data/lib/sequel/notion/filter_tables.rb +111 -0
- data/lib/sequel/notion/group_accumulator.rb +55 -0
- data/lib/sequel/notion/join_output.rb +58 -0
- data/lib/sequel/notion/join_where.rb +111 -0
- data/lib/sequel/notion/model_support.rb +52 -0
- data/lib/sequel/notion/notion_file.rb +228 -0
- data/lib/sequel/notion/page_api.rb +49 -0
- data/lib/sequel/notion/registry.rb +150 -0
- data/lib/sequel/notion/request_budget.rb +64 -0
- data/lib/sequel/notion/schema.rb +74 -0
- data/lib/sequel/notion/schema_lookup.rb +82 -0
- data/lib/sequel/notion/sort_compiler.rb +63 -0
- data/lib/sequel/notion/type_map.rb +125 -0
- data/lib/sequel/notion/type_map_builders.rb +125 -0
- data/lib/sequel/notion/type_map_dates.rb +67 -0
- data/lib/sequel/notion/type_map_extractors.rb +89 -0
- data/lib/sequel/notion/version.rb +7 -0
- data/lib/sequel/notion.rb +5 -0
- metadata +132 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "sequel/notion/filter_compiler"
|
|
4
|
+
require "sequel/notion/type_map"
|
|
5
|
+
|
|
6
|
+
module Sequel
|
|
7
|
+
module Notion
|
|
8
|
+
module SortCompiler
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def compile(order_clauses)
|
|
12
|
+
Array(order_clauses).map { |clause| compile_one(clause) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def compile_one(clause)
|
|
16
|
+
case clause
|
|
17
|
+
when Sequel::LiteralString then raise literal_error(clause)
|
|
18
|
+
when Sequel::SQL::OrderedExpression then compile_ordered(clause)
|
|
19
|
+
when Symbol, String, Sequel::SQL::Identifier,
|
|
20
|
+
Sequel::SQL::QualifiedIdentifier
|
|
21
|
+
compile_ascending(clause)
|
|
22
|
+
else
|
|
23
|
+
raise Sequel::Error,
|
|
24
|
+
"Unsupported order expression: #{clause.class}"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Sequel.lit is a String, but SQL, not a property name
|
|
29
|
+
def literal_error(clause)
|
|
30
|
+
Sequel::Error.new("Notion takes no SQL in an order: #{clause}")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Notion sorts empty values last in either direction, so only
|
|
34
|
+
# NULLS LAST (or no NULLS clause) can be honoured
|
|
35
|
+
def compile_ordered(clause)
|
|
36
|
+
if clause.nulls == :first
|
|
37
|
+
raise Sequel::Error,
|
|
38
|
+
"Notion sorts empty values last: #{clause.inspect}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
direction = clause.descending ? "descending" : "ascending"
|
|
42
|
+
{ "property" => sort_property(clause.expression),
|
|
43
|
+
"direction" => direction }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def compile_ascending(clause)
|
|
47
|
+
{ "property" => sort_property(clause),
|
|
48
|
+
"direction" => "ascending" }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Notion sorts by a property or a timestamp, never by the
|
|
52
|
+
# page's own id or in_trash, and rejects them with a 400
|
|
53
|
+
def sort_property(expr)
|
|
54
|
+
raise literal_error(expr) if expr.is_a?(Sequel::LiteralString)
|
|
55
|
+
|
|
56
|
+
name = FilterCompiler.property_name(expr)
|
|
57
|
+
return name unless TypeMap::PAGE_KEYS.include?(name)
|
|
58
|
+
|
|
59
|
+
raise Sequel::Error, "Notion cannot sort by #{name}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "notion_file"
|
|
4
|
+
require_relative "type_map_builders"
|
|
5
|
+
require_relative "type_map_dates"
|
|
6
|
+
require_relative "type_map_extractors"
|
|
7
|
+
|
|
8
|
+
module Sequel
|
|
9
|
+
module Notion
|
|
10
|
+
module TypeMap
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# Notion property types that the Notion API never accepts on write.
|
|
14
|
+
READ_ONLY_TYPES = %w[
|
|
15
|
+
formula rollup created_time last_edited_time
|
|
16
|
+
created_by last_edited_by unique_id button verification
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
# Keys every row carries for the page itself; a property of
|
|
20
|
+
# the same name would overwrite them
|
|
21
|
+
PAGE_KEYS = %w[id in_trash].freeze
|
|
22
|
+
|
|
23
|
+
# Writable Notion type => the builder for its value
|
|
24
|
+
BUILDERS = {
|
|
25
|
+
"title" => :build_rich_text,
|
|
26
|
+
"rich_text" => :build_rich_text,
|
|
27
|
+
"number" => :build_number,
|
|
28
|
+
"select" => :build_select,
|
|
29
|
+
"status" => :build_select,
|
|
30
|
+
"multi_select" => :build_multi_select,
|
|
31
|
+
"date" => :build_date,
|
|
32
|
+
"checkbox" => :build_checkbox,
|
|
33
|
+
"url" => :build_string,
|
|
34
|
+
"email" => :build_string,
|
|
35
|
+
"phone_number" => :build_string,
|
|
36
|
+
"relation" => :build_relation,
|
|
37
|
+
"people" => :build_people,
|
|
38
|
+
"files" => :build_files
|
|
39
|
+
}.freeze
|
|
40
|
+
|
|
41
|
+
# Notion page → flat Ruby hash
|
|
42
|
+
def page_to_row(page)
|
|
43
|
+
row = { id: page["id"], in_trash: page["in_trash"] }
|
|
44
|
+
props = page["properties"] || {}
|
|
45
|
+
check_property_names!(props.keys)
|
|
46
|
+
|
|
47
|
+
props.each do |name, prop|
|
|
48
|
+
row[name.to_sym] = extract_value(prop)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
row
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Flat Ruby hash → Notion properties payload
|
|
55
|
+
#
|
|
56
|
+
# +prop_types+ maps property NAME (String) to Notion TYPE
|
|
57
|
+
# (String), e.g. {"Name" => "title", "Status" => "status"}.
|
|
58
|
+
# +hash+ keys may be Symbols or Strings.
|
|
59
|
+
def row_to_properties(hash, prop_types)
|
|
60
|
+
hash.each_with_object({}) do |(key, value), props|
|
|
61
|
+
name = key.to_s
|
|
62
|
+
type = prop_types[name]
|
|
63
|
+
ensure_known_property(name, type, prop_types)
|
|
64
|
+
props[name] = build_named_property(name, value, type)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def check_property_names!(names)
|
|
69
|
+
clash = names & PAGE_KEYS
|
|
70
|
+
return if clash.empty?
|
|
71
|
+
|
|
72
|
+
raise Sequel::Error,
|
|
73
|
+
"property #{clash.first.inspect} clashes with the " \
|
|
74
|
+
"page's own column; rename it in Notion"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def ensure_known_property(name, type, prop_types)
|
|
78
|
+
return if type
|
|
79
|
+
|
|
80
|
+
raise Sequel::Error,
|
|
81
|
+
"unknown property #{name.inspect}; " \
|
|
82
|
+
"available: #{prop_types.keys.sort.inspect}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# build_property, wrapped so a failure names the property.
|
|
86
|
+
def build_named_property(name, value, type)
|
|
87
|
+
build_property(value, type)
|
|
88
|
+
rescue Sequel::Error => e
|
|
89
|
+
raise Sequel::Error,
|
|
90
|
+
"property #{name.inspect} (#{type.inspect}): " \
|
|
91
|
+
"#{e.message}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# ----------------------------------------------------------
|
|
95
|
+
# Notion → Ruby
|
|
96
|
+
# ----------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
def extract_value(prop)
|
|
99
|
+
type = prop["type"]
|
|
100
|
+
extractor = EXTRACTORS[type]
|
|
101
|
+
return send(extractor, prop) if extractor
|
|
102
|
+
|
|
103
|
+
prop[type]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# ----------------------------------------------------------
|
|
107
|
+
# Ruby → Notion
|
|
108
|
+
# ----------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
# Build a single Notion property payload for +value+ given its
|
|
111
|
+
# Notion +type+. Returns {TYPE => value}.
|
|
112
|
+
def build_property(value, type)
|
|
113
|
+
builder = BUILDERS[type]
|
|
114
|
+
return { type => send(builder, value) } if builder
|
|
115
|
+
|
|
116
|
+
if READ_ONLY_TYPES.include?(type)
|
|
117
|
+
raise Sequel::Error,
|
|
118
|
+
"property type #{type.inspect} is read-only"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
raise Sequel::Error, "unsupported property type #{type.inspect}"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "notion_file"
|
|
4
|
+
|
|
5
|
+
# Ruby → Notion builders for Sequel::Notion::TypeMap, split out here to
|
|
6
|
+
# keep type_map.rb's module short. See BUILDERS in type_map.rb for the
|
|
7
|
+
# Notion type => builder mapping that dispatches to these.
|
|
8
|
+
|
|
9
|
+
module Sequel
|
|
10
|
+
module Notion
|
|
11
|
+
module TypeMap
|
|
12
|
+
# Float() also accepts hex ("0x1A"), binary ("0b11") and
|
|
13
|
+
# underscore-grouped ("1_000") strings; a Notion number
|
|
14
|
+
# property is plain decimal text only.
|
|
15
|
+
DECIMAL = /\A\s*[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?\s*\z/
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def build_string(value) = value&.to_s
|
|
20
|
+
|
|
21
|
+
# title / rich_text: to_s, split into text objects of at most
|
|
22
|
+
# 2000 characters each, as Notion counts them (UTF-16 units)
|
|
23
|
+
def build_rich_text(value)
|
|
24
|
+
text_runs(value.to_s).map do |run|
|
|
25
|
+
{ "type" => "text", "text" => { "content" => run } }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# A character outside the BMP counts two and is never split
|
|
30
|
+
def text_runs(str)
|
|
31
|
+
units = 0
|
|
32
|
+
str.each_char.slice_before do |c|
|
|
33
|
+
n = c.ord > 0xFFFF ? 2 : 1
|
|
34
|
+
(units += n) > 2000 && (units = n)
|
|
35
|
+
end.map(&:join)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def build_number(value)
|
|
39
|
+
case value
|
|
40
|
+
when nil then nil
|
|
41
|
+
when Integer then value
|
|
42
|
+
when Float then finite_number(value)
|
|
43
|
+
when Numeric then finite_number(value.to_f)
|
|
44
|
+
when String then parse_number(value)
|
|
45
|
+
else raise Sequel::Error, "invalid number: #{value.inspect}"
|
|
46
|
+
end
|
|
47
|
+
rescue RangeError # a Complex has no Float
|
|
48
|
+
raise Sequel::Error, "invalid number: #{value.inspect}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# JSON has no NaN or Infinity
|
|
52
|
+
def finite_number(value)
|
|
53
|
+
return value if value.finite?
|
|
54
|
+
|
|
55
|
+
raise Sequel::Error, "invalid number: #{value.inspect}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Float overflows "1e400" to Infinity instead of raising; it
|
|
59
|
+
# also accepts hex/binary/underscored strings DECIMAL refuses
|
|
60
|
+
def parse_number(value)
|
|
61
|
+
raise Sequel::Error, "invalid number: #{value.inspect}" unless
|
|
62
|
+
value.match?(DECIMAL)
|
|
63
|
+
|
|
64
|
+
finite_number(Float(value))
|
|
65
|
+
rescue ArgumentError
|
|
66
|
+
raise Sequel::Error, "invalid number: #{value.inspect}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def build_select(value)
|
|
70
|
+
return nil if value.nil?
|
|
71
|
+
|
|
72
|
+
{ "name" => value.to_s }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def build_multi_select(value)
|
|
76
|
+
list(value).map { { "name" => it.to_s } }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def build_checkbox(value)
|
|
80
|
+
case value
|
|
81
|
+
when true, false
|
|
82
|
+
value
|
|
83
|
+
when nil
|
|
84
|
+
false
|
|
85
|
+
else
|
|
86
|
+
raise Sequel::Error,
|
|
87
|
+
"invalid checkbox value: #{value.inspect}"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# One id or an Array of them; nil is none
|
|
92
|
+
def build_relation(value) = list(value).map { { "id" => it } }
|
|
93
|
+
|
|
94
|
+
def build_people(value)
|
|
95
|
+
list(value).map { { "object" => "user", "id" => it } }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# One item or an Array of them; nil is none. Array() would
|
|
99
|
+
# turn a Hash into its pairs.
|
|
100
|
+
def list(value)
|
|
101
|
+
if value.is_a?(Hash)
|
|
102
|
+
raise Sequel::Error, "a list of names or ids takes no " \
|
|
103
|
+
"Hash: #{value.inspect}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
Array(value)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def build_files(value)
|
|
110
|
+
return [] if value.nil?
|
|
111
|
+
|
|
112
|
+
items = value.is_a?(Array) ? value : [value]
|
|
113
|
+
items.map { |item| build_file(item) }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def build_file(item)
|
|
117
|
+
case item
|
|
118
|
+
when File then item.to_notion
|
|
119
|
+
when String then File.external(item).to_notion
|
|
120
|
+
else raise Sequel::Error, "invalid file value: #{item.inspect}"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
# Ruby → Notion date builders for Sequel::Notion::TypeMap, split out of
|
|
6
|
+
# type_map_builders.rb to keep that module short.
|
|
7
|
+
|
|
8
|
+
module Sequel
|
|
9
|
+
module Notion
|
|
10
|
+
module TypeMap
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def build_date(value)
|
|
14
|
+
case value
|
|
15
|
+
when nil then nil
|
|
16
|
+
when String then { "start" => value }
|
|
17
|
+
when Range then build_date_range(value)
|
|
18
|
+
when Date, Time, DateTime then { "start" => value.iso8601 }
|
|
19
|
+
when Hash then build_date_hash(value)
|
|
20
|
+
else raise Sequel::Error, "invalid date value: #{value.inspect}"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Notion's date needs a start; an endless range leaves the end open
|
|
25
|
+
def build_date_range(value)
|
|
26
|
+
if value.begin.nil?
|
|
27
|
+
raise Sequel::Error,
|
|
28
|
+
"a date range needs a start: #{value.inspect}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
{ "start" => date_component(value.begin),
|
|
32
|
+
"end" => date_component(inclusive_end(value)) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Notion's end is inclusive: an exclusive range ends the day
|
|
36
|
+
# before, which only a Date end can say
|
|
37
|
+
def inclusive_end(range)
|
|
38
|
+
last = range.end
|
|
39
|
+
return last if last.nil? || !range.exclude_end?
|
|
40
|
+
return last.prev_day if last.instance_of?(Date)
|
|
41
|
+
|
|
42
|
+
raise Sequel::Error,
|
|
43
|
+
"an exclusive date range must end on a Date: " \
|
|
44
|
+
"#{range.inspect}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Like a Range, a Hash needs a start; nil is how a date is cleared
|
|
48
|
+
def build_date_hash(value)
|
|
49
|
+
start_v = value.key?(:start) ? value[:start] : value["start"]
|
|
50
|
+
end_v = value.key?(:end) ? value[:end] : value["end"]
|
|
51
|
+
|
|
52
|
+
if start_v.nil?
|
|
53
|
+
raise Sequel::Error,
|
|
54
|
+
"a date needs a start: #{value.inspect}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
result = { "start" => date_component(start_v) }
|
|
58
|
+
result["end"] = date_component(end_v) unless end_v.nil?
|
|
59
|
+
result
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def date_component(value)
|
|
63
|
+
value.respond_to?(:iso8601) ? value.iso8601 : value
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "notion_file"
|
|
4
|
+
|
|
5
|
+
# Notion → Ruby extractors for Sequel::Notion::TypeMap, split out here to
|
|
6
|
+
# keep type_map.rb's module short. Types not listed in EXTRACTORS fall
|
|
7
|
+
# back to extract_value's default: prop[prop["type"]].
|
|
8
|
+
|
|
9
|
+
module Sequel
|
|
10
|
+
module Notion
|
|
11
|
+
module TypeMap
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# Notion type => the extractor for its property hash. Only
|
|
15
|
+
# types whose value needs more than a plain prop[type] lookup
|
|
16
|
+
# are listed; see extract_value in type_map.rb for the rest.
|
|
17
|
+
EXTRACTORS = {
|
|
18
|
+
"title" => :extract_rich_text,
|
|
19
|
+
"rich_text" => :extract_rich_text,
|
|
20
|
+
"select" => :extract_select,
|
|
21
|
+
"status" => :extract_select,
|
|
22
|
+
"multi_select" => :extract_multi_select,
|
|
23
|
+
"date" => :extract_date,
|
|
24
|
+
"people" => :extract_people,
|
|
25
|
+
"relation" => :extract_relation,
|
|
26
|
+
"formula" => :extract_formula,
|
|
27
|
+
"files" => :extract_files,
|
|
28
|
+
"unique_id" => :extract_unique_id,
|
|
29
|
+
"rollup" => :extract_rollup
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
def extract_rich_text(prop)
|
|
33
|
+
prop[prop["type"]]&.map { it["plain_text"] }&.join
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def extract_select(prop)
|
|
37
|
+
prop.dig(prop["type"], "name")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def extract_multi_select(prop)
|
|
41
|
+
prop["multi_select"]&.map { it["name"] }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The start, or a Range of Strings when there is an end: the
|
|
45
|
+
# Range a write takes
|
|
46
|
+
def extract_date(prop)
|
|
47
|
+
date = prop["date"] || {}
|
|
48
|
+
return date["start"] unless date["end"]
|
|
49
|
+
|
|
50
|
+
date["start"]..date["end"]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Its value: a number or date, or an Array of the rolled-up
|
|
54
|
+
# values, each read as its own type
|
|
55
|
+
def extract_rollup(prop)
|
|
56
|
+
inner = prop["rollup"]
|
|
57
|
+
return if inner.nil?
|
|
58
|
+
return inner["array"].map { extract_value(it) } if
|
|
59
|
+
inner["type"] == "array"
|
|
60
|
+
|
|
61
|
+
extract_value(inner)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# As Notion displays it: "TK-62", or "62" with no prefix
|
|
65
|
+
def extract_unique_id(prop)
|
|
66
|
+
id = prop["unique_id"] || {}
|
|
67
|
+
return if id["number"].nil?
|
|
68
|
+
|
|
69
|
+
[id["prefix"], id["number"]].compact.join("-")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def extract_people(prop)
|
|
73
|
+
prop["people"]&.map { it["id"] }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def extract_relation(prop)
|
|
77
|
+
prop["relation"]&.map { it["id"] }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def extract_formula(prop)
|
|
81
|
+
extract_value(prop["formula"])
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def extract_files(prop)
|
|
85
|
+
File.from_notion_property(prop)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: sequel-notion
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Stéphane D'Alu
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: faraday
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: faraday-retry
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: sequel
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '5.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '5.0'
|
|
54
|
+
description: A Sequel database adapter that maps Notion databases and data sources
|
|
55
|
+
to Sequel's Dataset interface. Supports multi-source databases, filtering, sorting,
|
|
56
|
+
pagination, and CRUD operations via the Notion API (2026-03-11).
|
|
57
|
+
email:
|
|
58
|
+
- sdalu@sdalu.com
|
|
59
|
+
executables: []
|
|
60
|
+
extensions: []
|
|
61
|
+
extra_rdoc_files: []
|
|
62
|
+
files:
|
|
63
|
+
- CHANGELOG.md
|
|
64
|
+
- LICENSE.txt
|
|
65
|
+
- README.md
|
|
66
|
+
- lib/sequel/adapters/notion.rb
|
|
67
|
+
- lib/sequel/notion.rb
|
|
68
|
+
- lib/sequel/notion/dataset.rb
|
|
69
|
+
- lib/sequel/notion/dataset_aggregates.rb
|
|
70
|
+
- lib/sequel/notion/dataset_compounds.rb
|
|
71
|
+
- lib/sequel/notion/dataset_computed.rb
|
|
72
|
+
- lib/sequel/notion/dataset_grouping.rb
|
|
73
|
+
- lib/sequel/notion/dataset_having.rb
|
|
74
|
+
- lib/sequel/notion/dataset_joins.rb
|
|
75
|
+
- lib/sequel/notion/dataset_pages.rb
|
|
76
|
+
- lib/sequel/notion/dataset_selection.rb
|
|
77
|
+
- lib/sequel/notion/dataset_truncation.rb
|
|
78
|
+
- lib/sequel/notion/discovery.rb
|
|
79
|
+
- lib/sequel/notion/errors.rb
|
|
80
|
+
- lib/sequel/notion/filter_comparison.rb
|
|
81
|
+
- lib/sequel/notion/filter_compiler.rb
|
|
82
|
+
- lib/sequel/notion/filter_constants.rb
|
|
83
|
+
- lib/sequel/notion/filter_helpers.rb
|
|
84
|
+
- lib/sequel/notion/filter_like.rb
|
|
85
|
+
- lib/sequel/notion/filter_like_tokenizer.rb
|
|
86
|
+
- lib/sequel/notion/filter_negation.rb
|
|
87
|
+
- lib/sequel/notion/filter_nested.rb
|
|
88
|
+
- lib/sequel/notion/filter_nulls.rb
|
|
89
|
+
- lib/sequel/notion/filter_predicates.rb
|
|
90
|
+
- lib/sequel/notion/filter_shape.rb
|
|
91
|
+
- lib/sequel/notion/filter_tables.rb
|
|
92
|
+
- lib/sequel/notion/group_accumulator.rb
|
|
93
|
+
- lib/sequel/notion/join_output.rb
|
|
94
|
+
- lib/sequel/notion/join_where.rb
|
|
95
|
+
- lib/sequel/notion/model_support.rb
|
|
96
|
+
- lib/sequel/notion/notion_file.rb
|
|
97
|
+
- lib/sequel/notion/page_api.rb
|
|
98
|
+
- lib/sequel/notion/registry.rb
|
|
99
|
+
- lib/sequel/notion/request_budget.rb
|
|
100
|
+
- lib/sequel/notion/schema.rb
|
|
101
|
+
- lib/sequel/notion/schema_lookup.rb
|
|
102
|
+
- lib/sequel/notion/sort_compiler.rb
|
|
103
|
+
- lib/sequel/notion/type_map.rb
|
|
104
|
+
- lib/sequel/notion/type_map_builders.rb
|
|
105
|
+
- lib/sequel/notion/type_map_dates.rb
|
|
106
|
+
- lib/sequel/notion/type_map_extractors.rb
|
|
107
|
+
- lib/sequel/notion/version.rb
|
|
108
|
+
homepage: https://github.com/sdalu/sequel-notion
|
|
109
|
+
licenses:
|
|
110
|
+
- MIT
|
|
111
|
+
metadata:
|
|
112
|
+
homepage_uri: https://github.com/sdalu/sequel-notion
|
|
113
|
+
changelog_uri: https://github.com/sdalu/sequel-notion/blob/main/CHANGELOG.md
|
|
114
|
+
rubygems_mfa_required: 'true'
|
|
115
|
+
rdoc_options: []
|
|
116
|
+
require_paths:
|
|
117
|
+
- lib
|
|
118
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
119
|
+
requirements:
|
|
120
|
+
- - ">="
|
|
121
|
+
- !ruby/object:Gem::Version
|
|
122
|
+
version: '3.4'
|
|
123
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
124
|
+
requirements:
|
|
125
|
+
- - ">="
|
|
126
|
+
- !ruby/object:Gem::Version
|
|
127
|
+
version: '0'
|
|
128
|
+
requirements: []
|
|
129
|
+
rubygems_version: 4.0.16
|
|
130
|
+
specification_version: 4
|
|
131
|
+
summary: Sequel adapter for Notion data sources
|
|
132
|
+
test_files: []
|