pocketbase 0.1.1

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.
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PocketBase
4
+ module Services
5
+ # Base class for all API services (port of js-sdk BaseService).
6
+ class BaseService
7
+ attr_reader :client
8
+
9
+ # Send-level option keys; anything else in an options hash is treated
10
+ # as a query parameter (mirrors js-sdk normalizeUnknownQueryParams).
11
+ SEND_OPTION_KEYS = %i[
12
+ method headers body query params request_key timeout
13
+ ].freeze
14
+
15
+ def initialize(client)
16
+ @client = client
17
+ end
18
+
19
+ private
20
+
21
+ # Builds the options hash for Client#send_request:
22
+ # merges defaults with user options, folds :params into :query and
23
+ # moves unknown top-level keys into :query.
24
+ def send_options(defaults = {}, options = {})
25
+ normalized = {}
26
+ (options || {}).each { |k, v| normalized[k.to_sym] = v }
27
+ opts = defaults.merge(normalized)
28
+ query = {}
29
+ query.merge!(stringify_top(opts.delete(:params))) if opts[:params]
30
+ query.merge!(stringify_top(opts.delete(:query))) if opts[:query]
31
+
32
+ opts.keys.each do |key|
33
+ next if SEND_OPTION_KEYS.include?(key.to_sym)
34
+
35
+ query[key] = opts.delete(key)
36
+ end
37
+
38
+ opts[:query] = query unless query.empty?
39
+ opts
40
+ end
41
+
42
+ def stringify_top(hash)
43
+ result = {}
44
+ (hash || {}).each { |k, v| result[k] = v }
45
+ result
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PocketBase
4
+ module Services
5
+ # Batch service for transactional multi-requests (port of js-sdk BatchService).
6
+ #
7
+ # batch = pb.create_batch
8
+ # batch.collection("posts").create(title: "hello")
9
+ # batch.collection("posts").update("RECORD_ID", title: "hi")
10
+ # batch.collection("posts").delete("OTHER_ID")
11
+ # results = batch.send # => [{ "status" => 200, "body" => {...} }, ...]
12
+ class BatchService < BaseService
13
+ def initialize(client)
14
+ super
15
+ @requests = []
16
+ @subs = {}
17
+ end
18
+
19
+ # Starts constructing a batch request entry for the given collection.
20
+ def collection(collection_id_or_name)
21
+ key = collection_id_or_name.to_s
22
+ @subs[key] ||= SubBatchService.new(@requests, key, @client)
23
+ end
24
+
25
+ # Sends the queued batch requests.
26
+ def send(options = {})
27
+ form = MultipartBody.new
28
+ json_data = []
29
+
30
+ @requests.each_with_index do |req, i|
31
+ json_data << {
32
+ "method" => req[:method],
33
+ "url" => req[:url],
34
+ "headers" => req[:headers],
35
+ "body" => req[:json],
36
+ }
37
+ (req[:files] || {}).each do |key, files|
38
+ Array(files).each do |file|
39
+ info = Multipart.file_info(file, key.to_s)
40
+ form.add_file("requests.#{i}.#{key}", info[:filename], info[:content_type], info[:data])
41
+ end
42
+ end
43
+ end
44
+
45
+ form.add_json_payload({ "requests" => json_data })
46
+
47
+ opts = send_options({ method: "POST", body: form }, options)
48
+ @client.send_request("/api/batch", opts)
49
+ end
50
+ end
51
+
52
+ # Per-collection batch request builder.
53
+ class SubBatchService
54
+ def initialize(requests, collection_id_or_name, client)
55
+ @requests = requests
56
+ @collection_id_or_name = collection_id_or_name
57
+ @client = client
58
+ end
59
+
60
+ # Registers an upsert request (update when body has an existing id, else create).
61
+ def upsert(body_params = nil, options = {})
62
+ enqueue("PUT", base_records_path, body_params, options)
63
+ end
64
+
65
+ # Registers a create request.
66
+ def create(body_params = nil, options = {})
67
+ enqueue("POST", base_records_path, body_params, options)
68
+ end
69
+
70
+ # Registers an update request.
71
+ def update(id, body_params = nil, options = {})
72
+ enqueue("PATCH", "#{base_records_path}/#{Utils.encode_uri_component(id.to_s)}", body_params, options)
73
+ end
74
+
75
+ # Registers a delete request.
76
+ def delete(id, options = {})
77
+ enqueue("DELETE", "#{base_records_path}/#{Utils.encode_uri_component(id.to_s)}", nil, options)
78
+ end
79
+
80
+ private
81
+
82
+ def base_records_path
83
+ "/api/collections/#{Utils.encode_uri_component(@collection_id_or_name)}/records"
84
+ end
85
+
86
+ def enqueue(http_method, url, body_params, options)
87
+ options = (options || {}).dup
88
+ options[:body] = body_params || {}
89
+ request = { method: http_method, url: url }
90
+ prepare_request(request, options)
91
+ @requests << request
92
+ nil
93
+ end
94
+
95
+ def prepare_request(request, options)
96
+ query = {}
97
+ query.merge!(options.delete(:params)) if options[:params]
98
+ query.merge!(options.delete(:query)) if options[:query]
99
+ options.keys.each do |key|
100
+ next if BaseService::SEND_OPTION_KEYS.include?(key.to_sym)
101
+
102
+ query[key] = options.delete(key)
103
+ end
104
+ unless query.empty?
105
+ serialized = Utils.serialize_query_params(query)
106
+ request[:url] += (request[:url].include?("?") ? "&" : "?") + serialized unless serialized.empty?
107
+ end
108
+
109
+ request[:headers] = options[:headers]
110
+ request[:json] = {}
111
+ request[:files] = {}
112
+
113
+ body = options[:body] || {}
114
+ if body.is_a?(MultipartBody)
115
+ raise ArgumentError, "pass a Hash (not a MultipartBody) as batch sub-request body"
116
+ end
117
+
118
+ body.each do |key, val|
119
+ key = key.to_s
120
+ if Multipart.file_value?(val)
121
+ (request[:files][key] ||= []) << val
122
+ elsif val.is_a?(Array)
123
+ files = val.select { |v| Multipart.file_value?(v) }
124
+ regular = val.reject { |v| Multipart.file_value?(v) }
125
+ if files.any? && files.length == val.length
126
+ (request[:files][key] ||= []).concat(files)
127
+ else
128
+ request[:json][key] = regular
129
+ if files.any?
130
+ file_key = key
131
+ file_key += "+" unless key.start_with?("+") || key.end_with?("+")
132
+ (request[:files][file_key] ||= []).concat(files)
133
+ end
134
+ end
135
+ else
136
+ request[:json][key] = val
137
+ end
138
+ end
139
+ end
140
+
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PocketBase
4
+ module Services
5
+ # Collection management service (port of js-sdk CollectionService).
6
+ class CollectionService < CrudService
7
+ def base_crud_path
8
+ "/api/collections"
9
+ end
10
+
11
+ # Imports the provided collections.
12
+ # If delete_missing is true, local collections not present in the
13
+ # imported configuration WILL BE DELETED (including their records)!
14
+ def import(collections, delete_missing = false, options = {})
15
+ opts = send_options(
16
+ { method: "PUT",
17
+ body: { "collections" => collections, "deleteMissing" => delete_missing } },
18
+ options
19
+ )
20
+ @client.send_request("#{base_crud_path}/import", opts)
21
+ true
22
+ end
23
+
24
+ # Deletes all records of the given collection. Returns true.
25
+ def truncate(collection_id_or_name, options = {})
26
+ opts = send_options({ method: "DELETE" }, options)
27
+ @client.send_request(
28
+ "#{base_crud_path}/#{Utils.encode_uri_component(collection_id_or_name.to_s)}/truncate", opts
29
+ )
30
+ true
31
+ end
32
+
33
+ # Returns a type-indexed map of scaffolded collection models.
34
+ def get_scaffolds(options = {})
35
+ opts = send_options({ method: "GET" }, options)
36
+ @client.send_request("#{base_crud_path}/meta/scaffolds", opts)
37
+ end
38
+
39
+ # Returns the list of all configurable OAuth2 providers.
40
+ def get_all_oauth2_providers(options = {})
41
+ opts = send_options({ method: "GET" }, options)
42
+ @client.send_request("#{base_crud_path}/meta/oauth2-providers", opts)
43
+ end
44
+
45
+ # Executes a view query and returns a sample of the resulting records.
46
+ def dry_run_view_query(query, options = {})
47
+ opts = send_options({ method: "POST", body: { "query" => query } }, options)
48
+ @client.send_request("#{base_crud_path}/meta/dry-run-view", opts)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PocketBase
4
+ module Services
5
+ # Cron service (port of js-sdk CronService).
6
+ class CronService < BaseService
7
+ # Returns the list of all registered cron jobs.
8
+ def get_full_list(options = {})
9
+ opts = send_options({ method: "GET" }, options)
10
+ @client.send_request("/api/crons", opts)
11
+ end
12
+
13
+ # Runs the specified cron job. Returns true.
14
+ def run(job_id, options = {})
15
+ opts = send_options({ method: "POST" }, options)
16
+ @client.send_request("/api/crons/#{Utils.encode_uri_component(job_id.to_s)}", opts)
17
+ true
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PocketBase
4
+ module Services
5
+ # Base CRUD service (port of js-sdk CrudService).
6
+ class CrudService < BaseService
7
+ # Base path for the crud actions (without trailing slash).
8
+ def base_crud_path
9
+ raise NotImplementedError, "subclass must define #base_crud_path"
10
+ end
11
+
12
+ # Response data decoder (identity by default; override in subclasses).
13
+ def decode(data)
14
+ data
15
+ end
16
+
17
+ # Returns all list items batch-fetched at once
18
+ # (1000 items per request by default; change with `batch:`).
19
+ #
20
+ # service.get_full_list(batch: 200, filter: "...")
21
+ # service.get_full_list(200, filter: "...") # legacy positional form
22
+ def get_full_list(batch = nil, options = {})
23
+ if batch.is_a?(Hash)
24
+ options = batch.merge(options)
25
+ batch = nil
26
+ end
27
+ batch_size = batch || options.delete(:batch) || 1000
28
+ get_full_list_batched(batch_size, options)
29
+ end
30
+
31
+ # Returns a paginated items list.
32
+ #
33
+ # result = service.get_list(1, 30, filter: "...", sort: "-created")
34
+ # result["items"] # => Array of decoded items
35
+ def get_list(page = 1, per_page = 30, options = {})
36
+ opts = send_options({ method: "GET" }, options)
37
+ opts[:query] = { "page" => page, "perPage" => per_page }.merge(opts[:query] || {})
38
+ response = @client.send_request(base_crud_path, opts)
39
+ response["items"] = (response["items"] || []).map { |item| decode(item) }
40
+ response
41
+ end
42
+
43
+ # Returns the first item matching the filter (raises 404 when none found).
44
+ def get_first_list_item(filter, options = {})
45
+ opts = options.merge(
46
+ request_key: "one_by_filter_#{base_crud_path}_#{filter}",
47
+ filter: filter,
48
+ skipTotal: 1
49
+ )
50
+ result = get_list(1, 1, opts)
51
+ items = result["items"] || []
52
+ if items.empty?
53
+ raise ClientResponseError.new(
54
+ status: 404,
55
+ response: { "code" => 404, "message" => "The requested resource wasn't found.", "data" => {} }
56
+ )
57
+ end
58
+
59
+ items[0]
60
+ end
61
+
62
+ # Returns a single item by id (raises 404 when id is empty).
63
+ def get_one(id, options = {})
64
+ if id.nil? || id.to_s.empty?
65
+ raise ClientResponseError.new(
66
+ url: @client.build_url(base_crud_path + "/"),
67
+ status: 404,
68
+ response: { "code" => 404, "message" => "Missing required record id.", "data" => {} }
69
+ )
70
+ end
71
+
72
+ opts = send_options({ method: "GET" }, options)
73
+ decode(@client.send_request("#{base_crud_path}/#{Utils.encode_uri_component(id.to_s)}", opts))
74
+ end
75
+
76
+ # Creates a new item. Pass a File/IO value inside body for file uploads.
77
+ def create(body_params = nil, options = {})
78
+ opts = send_options({ method: "POST", body: body_params }, options)
79
+ decode(@client.send_request(base_crud_path, opts))
80
+ end
81
+
82
+ # Updates an existing item by id.
83
+ def update(id, body_params = nil, options = {})
84
+ opts = send_options({ method: "PATCH", body: body_params }, options)
85
+ decode(@client.send_request("#{base_crud_path}/#{Utils.encode_uri_component(id.to_s)}", opts))
86
+ end
87
+
88
+ # Deletes an item by id. Returns true on success.
89
+ def delete(id, options = {})
90
+ opts = send_options({ method: "DELETE" }, options)
91
+ @client.send_request("#{base_crud_path}/#{Utils.encode_uri_component(id.to_s)}", opts)
92
+ true
93
+ end
94
+
95
+ private
96
+
97
+ def get_full_list_batched(batch_size, options)
98
+ options = (options || {}).merge(skipTotal: 1)
99
+ result = []
100
+ page = 1
101
+ loop do
102
+ list = get_list(page, batch_size || 1000, options)
103
+ items = list["items"] || []
104
+ result.concat(items)
105
+ break if items.length < list["perPage"].to_i
106
+
107
+ page += 1
108
+ end
109
+ result
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PocketBase
4
+ module Services
5
+ # File service (port of js-sdk FileService).
6
+ class FileService < BaseService
7
+ # Builds an absolute record file URL for the given filename.
8
+ #
9
+ # pb.files.get_url(record, "avatar.png", thumb: "100x100", download: true)
10
+ def get_url(record, filename, query_params = {})
11
+ record ||= {}
12
+ collection = record["collectionId"] || record[:collectionId] ||
13
+ record["collectionName"] || record[:collectionName]
14
+ id = record["id"] || record[:id]
15
+ return "" if filename.nil? || filename.to_s.empty? || id.nil? || collection.nil?
16
+
17
+ parts = ["api", "files",
18
+ Utils.encode_uri_component(collection.to_s),
19
+ Utils.encode_uri_component(id.to_s),
20
+ Utils.encode_uri_component(filename.to_s)]
21
+ result = @client.build_url(parts.join("/"))
22
+
23
+ query_params.delete(:download) if query_params[:download] == false
24
+ query_params.delete("download") if query_params["download"] == false
25
+
26
+ params = Utils.serialize_query_params(query_params)
27
+ result += (result.include?("?") ? "&" : "?") + params unless params.empty?
28
+ result
29
+ end
30
+
31
+ # Requests a new private file access token for the current auth model.
32
+ def get_token(options = {})
33
+ opts = send_options({ method: "POST" }, options)
34
+ data = @client.send_request("/api/files/token", opts)
35
+ data["token"] || data[:token] || ""
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PocketBase
4
+ module Services
5
+ # Health service (port of js-sdk HealthService).
6
+ class HealthService < BaseService
7
+ # Checks the health status of the API.
8
+ def check(options = {})
9
+ opts = send_options({ method: "GET" }, options)
10
+ @client.send_request("/api/health", opts)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PocketBase
4
+ module Services
5
+ # Logs service (port of js-sdk LogService).
6
+ class LogService < BaseService
7
+ # Returns a paginated logs list.
8
+ def get_list(page = 1, per_page = 30, options = {})
9
+ opts = send_options({ method: "GET" }, options)
10
+ opts[:query] = { "page" => page, "perPage" => per_page }.merge(opts[:query] || {})
11
+ @client.send_request("/api/logs", opts)
12
+ end
13
+
14
+ # Returns a single log by id (raises 404 when id is empty).
15
+ def get_one(id, options = {})
16
+ if id.nil? || id.to_s.empty?
17
+ raise ClientResponseError.new(
18
+ url: @client.build_url("/api/logs/"),
19
+ status: 404,
20
+ response: { "code" => 404, "message" => "Missing required log id.", "data" => {} }
21
+ )
22
+ end
23
+
24
+ opts = send_options({ method: "GET" }, options)
25
+ @client.send_request("/api/logs/#{Utils.encode_uri_component(id.to_s)}", opts)
26
+ end
27
+
28
+ # Returns logs statistics.
29
+ def get_stats(options = {})
30
+ opts = send_options({ method: "GET" }, options)
31
+ @client.send_request("/api/logs/stats", opts)
32
+ end
33
+
34
+ # Deletes all logs. Returns true.
35
+ def truncate(options = {})
36
+ opts = send_options({ method: "DELETE" }, options)
37
+ @client.send_request("/api/logs", opts)
38
+ true
39
+ end
40
+ end
41
+ end
42
+ end