notion 1.0.5 → 1.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 529d9a75b50731156f263184b27628a2ee6b0b0cd798e3f3b44e5ea3f7106c60
4
- data.tar.gz: eb5b69ebe9b9d34215d6da17e8c2999761e9242b2618afaaa6583ee09691c65d
3
+ metadata.gz: 3114ccf59f80a17322c559fce61050e7e7f1908a279e8786c4a8fe87e6fa79d8
4
+ data.tar.gz: a7c6fea5eb18a4d962117a4a5120bc357e0bf038640ba39db821d5bc3de230e6
5
5
  SHA512:
6
- metadata.gz: '09ec6f516f22fac9f2b9c7c2efc3df164f83a745414cb18f092a6df939625435a77883712b86e100d7062536b189ddd8115145a52528cd5cc81767029f3bd250'
7
- data.tar.gz: b643a46d9682d2ebb612c0d0c37454344f0f9abb580c1cf258887bc7a242afdc22af155c78b87434a00c38a048b206351ad453477d5c914393b1ff4ac68aeb97
6
+ metadata.gz: '06202342711853b35c0d694b4de7c89aec269f226d5ceda2c6b4e086f61f6e8f3e7381ca1d0389d85c2b3814d849dd980d9d6b3492a9d2a3d9c8bf9fbad71939'
7
+ data.tar.gz: 7561a58fff0a26b4190a79c713a4f89c3f85c1c5cca4bca21e2c9013ea3fa981f32e8779e9b771b243fe41a65c5b717f9a09e675e9c9590c3a09cf1f344ca02b
data/README.md CHANGED
@@ -310,8 +310,12 @@ From here, you can instantiate the Notion Client with the following code:
310
310
  )
311
311
  ```
312
312
  ### Retrieve a full-page Collection View
313
- Currently, either a "normal" Page URL or the Page Block ID is accepted to the `get_page` method. Therefore, if you pass the full URL to the CV Table, it will raise an error:
314
- ```text
315
- the URL or ID passed to the get_page method must be that of a Page Block.
313
+ A full-page collection view must have a URL that follows the below pattern:
314
+ https://www.notion.so/danmurphy/[page-id]?v=[view-id]
315
+ Then, it can be retrieved with the following code:
316
+ ```ruby
317
+ >>> @client = NotionAPI::Client.new(
318
+ "<insert_v2_token_here>"
319
+ )
320
+ >>> @client.get_page(https://www.notion.so/danmurphy/[page-id]?v=[view-id])
316
321
  ```
317
- To avoid this, you must pass only the ID of the full-page collection-view to the `get_page` method. This is next up on the features list, so passing the full URL will be supported soon:smile:
@@ -235,17 +235,26 @@ module NotionAPI
235
235
  # ! parse and clean the URL or ID object provided.
236
236
  # ! url_or_id -> the block ID or URL : ``str``
237
237
  http_or_https = url_or_id.match(/^(http|https)/) # true if http or https in url_or_id...
238
+ collection_view_match = url_or_id.match(/(\?v=)/)
239
+
238
240
  if (url_or_id.length == 36) && ((url_or_id.split('-').length == 5) && !http_or_https)
239
241
  # passes if url_or_id is perfectly formatted already...
240
242
  url_or_id
241
- elsif (http_or_https && (url_or_id.split('-').last.length == 32)) || (!http_or_https && (url_or_id.length == 32))
243
+ elsif (http_or_https && (url_or_id.split('-').last.length == 32)) || (!http_or_https && (url_or_id.length == 32)) || (collection_view_match)
242
244
  # passes if either:
243
245
  # 1. a URL is passed as url_or_id and the ID at the end is 32 characters long or
244
246
  # 2. a URL is not passed and the ID length is 32 [aka unformatted]
245
247
  pattern = [8, 13, 18, 23]
246
- id = url_or_id.split('-').last
247
- pattern.each { |index| id.insert(index, '-') }
248
- id
248
+ if collection_view_match
249
+ id_without_view = url_or_id.split('?')[0]
250
+ clean_id = id_without_view.split('/').last
251
+ pattern.each { |index| clean_id.insert(index, '-') }
252
+ clean_id
253
+ else
254
+ id = url_or_id.split('-').last
255
+ pattern.each { |index| id.insert(index, '-') }
256
+ id
257
+ end
249
258
  else
250
259
  raise ArgumentError, 'Expected a Notion page URL or a page ID. Please consult the documentation for further information.'
251
260
  end
@@ -33,7 +33,9 @@ module NotionAPI
33
33
  transaction_id = extract_id(SecureRandom.hex(16))
34
34
  space_id = extract_id(SecureRandom.hex(16))
35
35
  new_block_id = extract_id(SecureRandom.hex(16))
36
- schema = extract_collection_schema(@collection_id, @view_id)
36
+ collection_data = extract_collection_data(collection_id, view_id)
37
+ last_row_id = collection_data["collection_view"][@view_id]["value"]["page_sort"][-1]
38
+ schema = collection_data["collection"][collection_id]["value"]["schema"]
37
39
  keys = schema.keys
38
40
  col_map = {}
39
41
  keys.map { |key| col_map[schema[key]["name"]] = key }
@@ -47,17 +49,28 @@ module NotionAPI
47
49
  instantiate_row = Utils::CollectionViewComponents.add_new_row(new_block_id)
48
50
  set_block_alive = Utils::CollectionViewComponents.set_collection_blocks_alive(new_block_id, @collection_id)
49
51
  new_block_edited_time = Utils::BlockComponents.last_edited_time(new_block_id)
50
- parent_edited_time = Utils::BlockComponents.last_edited_time(@parent_id)
52
+ page_sort = Utils::BlockComponents.row_location_add(last_row_id, new_block_id, @view_id)
51
53
 
52
54
  operations = [
53
55
  instantiate_row,
54
56
  set_block_alive,
55
57
  new_block_edited_time,
56
- parent_edited_time,
58
+ page_sort,
57
59
  ]
58
60
 
59
61
  data.keys.each_with_index do |col_name, j|
60
- child_component = Utils::CollectionViewComponents.insert_data(new_block_id, j.zero? ? "title" : col_map[col_name], data[col_name], j.zero? ? schema["title"]["type"] : schema[col_map[col_name]]["type"])
62
+ unless col_map.keys.include?(col_name.to_s); raise ArgumentError, "Column '#{col_name.to_s}' does not exist." end
63
+ if %q[select multi_select].include?(schema[col_map[col_name.to_s]]["type"])
64
+ options = schema[col_map[col_name.to_s]]["options"].nil? ? [] : schema[col_map[col_name.to_s]]["options"].map { |option| option["value"] }
65
+ multi_select_multi_options = data[col_name].split(",")
66
+ multi_select_multi_options.each do |option|
67
+ if !options.include?(option.strip)
68
+ create_new_option = Utils::CollectionViewComponents.add_new_option(col_map[col_name.to_s], option.strip, @collection_id)
69
+ operations.push(create_new_option)
70
+ end
71
+ end
72
+ end
73
+ child_component = Utils::CollectionViewComponents.insert_data(new_block_id, col_map[col_name.to_s], data[col_name], schema[col_map[col_name.to_s]]["type"])
61
74
  operations.push(child_component)
62
75
  end
63
76
 
@@ -73,7 +86,18 @@ module NotionAPI
73
86
  unless response.code == 200; raise "There was an issue completing your request. Here is the response from Notion: #{response.body}, and here is the payload that was sent: #{operations}.
74
87
  Please try again, and if issues persist open an issue in GitHub."; end
75
88
 
76
- NotionAPI::CollectionViewRow.new(new_block_id, @parent_id, @collection_id, @view_id)
89
+ collection_row = NotionAPI::CollectionViewRow.new(new_block_id, @parent_id, @collection_id, @view_id)
90
+
91
+ properties = {}
92
+ data.keys.each do |col|
93
+ properties[col_map[col.to_s]] = [[data[col]]]
94
+ end
95
+
96
+ collection_data["block"][collection_row.id] = { "role" => "editor", "value" => { "id" => collection_row.id, "version" => 12, "type" => "page", "properties" => properties, "created_time" => 1607253360000, "last_edited_time" => 1607253360000, "parent_id" => "dde513c6-2428-4a5d-a830-7a67fdbf6b48", "parent_table" => "collection", "alive" => true, "created_by_table" => "notion_user", "created_by_id" => "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", "last_edited_by_table" => "notion_user", "last_edited_by_id" => "0c5f02f3-495d-4b73-b1c5-9f6fe03a8c26", "shard_id" => 955090, "space_id" => "f687f7de-7f4c-4a86-b109-941a8dae92d2" } }
97
+ row_data = collection_data["block"][collection_row.id]
98
+ create_singleton_methods_and_instance_variables(collection_row, row_data)
99
+
100
+ collection_row
77
101
  end
78
102
 
79
103
  def add_property(name, type)
@@ -204,6 +228,7 @@ module NotionAPI
204
228
  end
205
229
  clean_row_instances
206
230
  end
231
+
207
232
  def create_singleton_methods_and_instance_variables(row, row_data)
208
233
  # ! creates singleton methods for each property in a CollectionView.
209
234
  # ! row -> the block ID of the 'row' to retrieve: ``str``
@@ -213,22 +238,25 @@ module NotionAPI
213
238
  column_mappings = schema.keys
214
239
  column_hash = {}
215
240
  column_names = column_mappings.map { |mapping| column_hash[mapping] = schema[mapping]["name"].downcase }
216
-
241
+
217
242
  column_hash.keys.each_with_index do |column, i|
218
243
  # loop over the column names...
219
244
  # set instance variables for each column, allowing the dev to 'read' the column value
220
- cleaned_column = column_hash[column].split(" ").join("_").downcase.to_sym
245
+ cleaned_column = clean_property_names(column_hash, column)
221
246
 
222
247
  # p row_data["value"]["properties"][column_mappings[i]], !(row_data["value"]["properties"][column] or row_data["value"]["properties"][column_mappings[i]])
223
248
  if row_data["value"]["properties"].nil? or row_data["value"]["properties"][column].nil?
224
249
  value = ""
225
250
  else
226
- value = row_data["value"]["properties"][column][0][0]
251
+ value = row_data["value"]["properties"][column][0][0]
252
+ if ["‣"].include?(value.to_s)
253
+ value = row_data["value"]["properties"][column][0][1].flatten[-1]
254
+ end
227
255
  end
228
256
 
229
257
  row.instance_variable_set("@#{cleaned_column}", value)
230
258
  CollectionViewRow.class_eval { attr_reader cleaned_column }
231
- # then, define singleton methods for each column that are used to update the table cell
259
+ # then, define singleton methods for each column that are used to update the table cell
232
260
  row.define_singleton_method("#{cleaned_column}=") do |new_value|
233
261
  # neat way to get the name of the currently invoked method...
234
262
  parsed_method = __method__.to_s[0...-1].split("_").join(" ")
@@ -245,12 +273,23 @@ module NotionAPI
245
273
  space_id: space_id,
246
274
  }
247
275
 
248
- update_property_value = Utils::CollectionViewComponents.update_property_value(@id, column_hash.key(parsed_method), new_value)
276
+ update_property_value_hash = Utils::CollectionViewComponents.update_property_value(@id, column_hash.key(parsed_method), new_value)
249
277
 
250
278
  operations = [
251
- update_property_value,
279
+ update_property_value_hash,
252
280
  ]
253
281
 
282
+ if %q[select multi_select].include?(schema[column_hash.key(parsed_method)]["type"])
283
+ options = schema[column_hash.key(parsed_method)]["options"].nil? ? [] : schema[column_hash.key(parsed_method)]["options"].map { |option| option["value"] }
284
+ multi_select_multi_options = new_value.split(",")
285
+ multi_select_multi_options.each do |option|
286
+ if !options.include?(option.strip)
287
+ create_new_option = Utils::CollectionViewComponents.add_new_option(column_hash.key(parsed_method), option.strip, @collection_id)
288
+ operations.push(create_new_option)
289
+ end
290
+ end
291
+ end
292
+
254
293
  request_url = URLS[:UPDATE_BLOCK]
255
294
  request_body = build_payload(operations, request_ids)
256
295
  response = HTTParty.post(
@@ -260,14 +299,23 @@ module NotionAPI
260
299
  headers: headers,
261
300
  )
262
301
  unless response.code == 200; raise "There was an issue completing your request. Here is the response from Notion: #{response.body}, and here is the payload that was sent: #{operations}.
263
- Please try again, and if issues persist open an issue in GitHub.";end
264
-
302
+ Please try again, and if issues persist open an issue in GitHub."; end
303
+
265
304
  # set the instance variable to the updated value!
266
305
  _ = row.instance_variable_set("@#{__method__.to_s[0...-1]}", new_value)
267
306
  row
268
307
  end
269
308
  end
270
309
  end
310
+
311
+ def clean_property_names(prop_hash, prop_notion_name)
312
+ # ! standardize property names by splitting the words in the property name into an array, removing non-alphanumeric
313
+ # ! characters, downcasing, and then re-joining the array with underscores.
314
+ # ! prop_hash -> hash of property notion names and property textual names: ``str``
315
+ # ! prop_notion_name -> the four-character long name of the notion property: ``str``
316
+
317
+ prop_hash[prop_notion_name].split(" ").map { |word| word.gsub(/[^a-z0-9]/i, "").downcase }.join("_").to_sym
318
+ end
271
319
  end
272
320
 
273
321
  # class that represents each row in a CollectionView
@@ -200,14 +200,14 @@ module Utils
200
200
 
201
201
  args = if command == "listAfter"
202
202
  {
203
- after: target || block_id,
204
- id: new_block_id || block_id,
205
- }
203
+ after: target || block_id,
204
+ id: new_block_id || block_id,
205
+ }
206
206
  else
207
207
  {
208
- before: target || block_id,
209
- id: new_block_id || block_id,
210
- }
208
+ before: target || block_id,
209
+ id: new_block_id || block_id,
210
+ }
211
211
  end
212
212
 
213
213
  {
@@ -219,6 +219,21 @@ module Utils
219
219
  }
220
220
  end
221
221
 
222
+ def self.row_location_add(last_row_id, block_id, view_id)
223
+ {
224
+ "table": "collection_view",
225
+ "id": view_id,
226
+ "path": [
227
+ "page_sort"
228
+ ],
229
+ "command": "listAfter",
230
+ "args": {
231
+ "after": last_row_id,
232
+ "id": block_id
233
+ }
234
+ }
235
+ end
236
+
222
237
  def self.block_location_remove(block_parent_id, block_id)
223
238
  # ! removes a notion block
224
239
  # ! block_parent_id -> the parent ID of the block to remove : ``str``
@@ -276,9 +291,9 @@ module Utils
276
291
  def self.add_emoji_icon(block_id, icon)
277
292
  {
278
293
  id: block_id,
279
- table:"block",
280
- path:["format","page_icon"],
281
- command:"set","args": icon
294
+ table: "block",
295
+ path: ["format", "page_icon"],
296
+ command: "set", "args": icon,
282
297
  }
283
298
  end
284
299
  end
@@ -383,14 +398,14 @@ module Utils
383
398
  # ! new_block_id -> id of the new block
384
399
  # ! data -> json data to insert into table.
385
400
  col_names = data[0].keys
386
- data_mappings = {Integer => "number", String => "text", Array => "text", Float => "number", Date => "date"}
401
+ data_mappings = { Integer => "number", String => "text", Array => "text", Float => "number", Date => "date" }
387
402
  exceptions = [ArgumentError, TypeError]
388
403
  data_types = col_names.map do |name|
389
404
  # TODO: this is a little hacky... should probably think about a better way or add a requirement for user input to match a certain criteria.
390
- begin
405
+ begin
391
406
  DateTime.parse(data[0][name]) ? data_mappings[Date] : nil
392
407
  rescue *exceptions
393
- data_mappings[data[0][name].class]
408
+ data_mappings[data[0][name].class]
394
409
  end
395
410
  end
396
411
 
@@ -403,18 +418,18 @@ module Utils
403
418
  end
404
419
  end
405
420
  return {
406
- id: collection_id,
407
- table: "collection",
408
- path: [],
409
- command: "update",
410
- args: {
411
- id: collection_id,
412
- schema: schema_conf,
413
- parent_id: new_block_id,
414
- parent_table: "block",
415
- alive: true,
416
- },
417
- }, data_types
421
+ id: collection_id,
422
+ table: "collection",
423
+ path: [],
424
+ command: "update",
425
+ args: {
426
+ id: collection_id,
427
+ schema: schema_conf,
428
+ parent_id: new_block_id,
429
+ parent_table: "block",
430
+ alive: true,
431
+ },
432
+ }, data_types
418
433
  end
419
434
 
420
435
  def self.set_collection_title(collection_title, collection_id)
@@ -440,6 +455,11 @@ module Utils
440
455
  # ! column -> the name of the column to insert data into.
441
456
  # ! value -> the value to insert into the column.
442
457
  # ! mapping -> the column data type.
458
+ simple_mappings = ["title", "text", "phone_number", "email", "url", "number", "checkbox", "select", "multi_select"]
459
+ datetime_mappings = ["date"]
460
+ media_mappings = ["file"]
461
+ person_mappings = ["person"]
462
+
443
463
  table = "block"
444
464
  path = [
445
465
  "properties",
@@ -447,12 +467,50 @@ module Utils
447
467
  ]
448
468
  command = "set"
449
469
 
470
+ if simple_mappings.include?(mapping)
471
+ args = [[value]]
472
+ elsif media_mappings.include?(mapping)
473
+ args = [[value, [["a", value]]]]
474
+ elsif datetime_mappings.include?(mapping)
475
+ args = [["‣", [["d", { "type": "date", "start_date": value }]]]]
476
+ elsif person_mappings.include?(mapping)
477
+ args = [["‣",
478
+ [["u", value]]
479
+ ]]
480
+ else
481
+ raise SchemaTypeError, "Invalid property type: #{mapping}"
482
+ end
483
+
450
484
  {
451
- id: block_id,
452
485
  table: table,
486
+ id: block_id,
487
+ command: command,
453
488
  path: path,
489
+ args: args,
490
+ }
491
+ end
492
+
493
+ def self.add_new_option(column, value, collection_id)
494
+ table = "collection"
495
+ path = ["schema", column, "options"]
496
+ command = "keyedObjectListAfter"
497
+ colors = ["default", "gray", "brown", "orange", "yellow", "green", "blue", "purple", "pink", "red"]
498
+ random_color = colors[rand(0...colors.length)]
499
+
500
+ args = {
501
+ "value": {
502
+ "id": SecureRandom.hex(16),
503
+ "value": value,
504
+ "color": random_color
505
+ }
506
+ }
507
+
508
+ {
509
+ table: table,
510
+ id: collection_id,
454
511
  command: command,
455
- args: mapping == "date" ? [["‣",[["d",{"type": "date","start_date": value}]]]] : [[value]],
512
+ path: path,
513
+ args: args,
456
514
  }
457
515
  end
458
516
 
@@ -505,45 +563,45 @@ module Utils
505
563
  args["format"] = {
506
564
  "table_properties" => [
507
565
  {
508
- "property" => "title",
509
- "visible" => true,
510
- "width" => 280,
511
- },
566
+ "property" => "title",
567
+ "visible" => true,
568
+ "width" => 280,
569
+ },
512
570
  {
513
- "property" => "aliases",
514
- "visible" => true,
515
- "width" => 200,
516
- },
571
+ "property" => "aliases",
572
+ "visible" => true,
573
+ "width" => 200,
574
+ },
517
575
  {
518
- "property" => "category",
519
- "visible" => true,
520
- "width" => 200,
521
- },
576
+ "property" => "category",
577
+ "visible" => true,
578
+ "width" => 200,
579
+ },
522
580
  {
523
- "property" => "description",
524
- "visible" => true,
525
- "width" => 200,
526
- },
581
+ "property" => "description",
582
+ "visible" => true,
583
+ "width" => 200,
584
+ },
527
585
  {
528
- "property" => "ios_version",
529
- "visible" => true,
530
- "width" => 200,
531
- },
586
+ "property" => "ios_version",
587
+ "visible" => true,
588
+ "width" => 200,
589
+ },
590
+ {
591
+ "property" => "tags",
592
+ "visible" => true,
593
+ "width" => 200,
594
+ },
532
595
  {
533
- "property" => "tags",
534
- "visible" => true,
535
- "width" => 200,
536
- },
537
- {
538
596
  "property" => "phone",
539
597
  "visible" => true,
540
598
  "width" => 200,
541
599
  },
542
600
  {
543
- "property" => "unicode_version",
544
- "visible" => true,
545
- "width" => 200,
546
- }
601
+ "property" => "unicode_version",
602
+ "visible" => true,
603
+ "width" => 200,
604
+ },
547
605
  ],
548
606
  }
549
607
  {
@@ -563,11 +621,11 @@ module Utils
563
621
  table = "block"
564
622
  path = [
565
623
  "properties",
566
- column_name
624
+ column_name,
567
625
  ]
568
626
  command = "set"
569
627
  args = [[
570
- new_value
628
+ new_value,
571
629
  ]]
572
630
 
573
631
  {
@@ -575,11 +633,18 @@ module Utils
575
633
  table: table,
576
634
  path: path,
577
635
  command: command,
578
- args: args
636
+ args: args,
579
637
  }
580
638
  end
581
639
  end
582
640
 
641
+ class SchemaTypeError < StandardError
642
+ def initialize(msg="Custom exception that is raised when an invalid property type is passed as a mapping.", exception_type="schema_type")
643
+ @exception_type = exception_type
644
+ super(msg)
645
+ end
646
+ end
647
+
583
648
  def build_payload(operations, request_ids)
584
649
  # ! properly formats the payload for Notions backend.
585
650
  # ! operations -> an array of hashes that define the operations to perform : ``Array[Hash]``
@@ -600,4 +665,4 @@ module Utils
600
665
  }
601
666
  payload
602
667
  end
603
- end
668
+ end
@@ -1,3 +1,3 @@
1
1
  module NotionAPI
2
- VERSION = '1.0.5'
2
+ VERSION = '1.1.2'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: notion
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.5
4
+ version: 1.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dan Murphy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-04 00:00:00.000000000 Z
11
+ date: 2020-12-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty