fmrest-spyke 0.13.1 → 0.16.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 +4 -4
- data/.yardopts +1 -0
- data/CHANGELOG.md +21 -0
- data/README.md +128 -83
- data/lib/fmrest/spyke.rb +1 -7
- data/lib/fmrest/spyke/base.rb +15 -0
- data/lib/fmrest/spyke/model/orm.rb +4 -4
- data/lib/fmrest/spyke/model/serialization.rb +12 -26
- data/lib/fmrest/spyke/model/uri.rb +17 -3
- data/lib/fmrest/spyke/relation.rb +183 -6
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1a91619fdca8717119b8b299648d3388948551e653bececb0a90a422850be0a2
|
4
|
+
data.tar.gz: 9e666b8f8162522b1269a3b4d18cda8724ad7439d0398672c02d228149cb636d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6fee0ccdc7bb75ae47f620c0cbf3da137806394225a565f5227d9a63e4ea4612b78200997dfc84594ba4eb2db84a156a28d3b05109d9dfb95f6851434bd94f6b
|
7
|
+
data.tar.gz: bab901d8024b1331358325f149ebc297c206e549aba81b73187748b2f168c7eecac2409170722e7a062901317084348c4ac3811bddffa07613bf944aa0f61f00
|
data/.yardopts
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,26 @@
|
|
1
1
|
## Changelog
|
2
2
|
|
3
|
+
### 0.16.0
|
4
|
+
|
5
|
+
* Add `FmRest.logger=`
|
6
|
+
* Handle serialization of `nil`, `true` and `false` values
|
7
|
+
|
8
|
+
### 0.15.2
|
9
|
+
|
10
|
+
* Fix autoloading of `FmRest::Layout`
|
11
|
+
|
12
|
+
### 0.15.0
|
13
|
+
|
14
|
+
* Much improved querying API (see documentation on querying), adding new
|
15
|
+
`.query` capabilities, as well as two new methods: `.match` and `.or`
|
16
|
+
|
17
|
+
### 0.14.0
|
18
|
+
|
19
|
+
* Aliased `FmRest::Spyke::Base` as `FmRest::Layout` (now preferred), and
|
20
|
+
provided a shortcut version for setting the layout name (e.g. `class Foo <
|
21
|
+
FmRest::Layout("LayoutName")`)
|
22
|
+
* Made `layout` class setting subclass-inheritable
|
23
|
+
|
3
24
|
### 0.13.1
|
4
25
|
|
5
26
|
* Fix downloading of container field data from FMS19+
|
data/README.md
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
[](https://rubygems.org/gems/fmrest)
|
4
4
|

|
5
|
+
[](https://rubydoc.info/github/beezwax/fmrest-ruby)
|
5
6
|
|
6
7
|
A Ruby client for
|
7
8
|
[FileMaker 18 and 19's Data API](https://help.claris.com/en/data-api-guide)
|
@@ -9,7 +10,7 @@ using
|
|
9
10
|
[Faraday](https://github.com/lostisland/faraday) and with optional
|
10
11
|
ActiveRecord-ish ORM features through [Spyke](https://github.com/balvig/spyke).
|
11
12
|
|
12
|
-
fmrest-ruby only partially implements FileMaker
|
13
|
+
fmrest-ruby only partially implements FileMaker 19's Data API.
|
13
14
|
See the [implementation completeness table](#api-implementation-completeness-table)
|
14
15
|
to see if a feature you need is natively supported by the gem.
|
15
16
|
|
@@ -17,10 +18,10 @@ to see if a feature you need is natively supported by the gem.
|
|
17
18
|
|
18
19
|
The `fmrest` gem is a wrapper for two other gems:
|
19
20
|
|
20
|
-
* `fmrest-
|
21
|
+
* `fmrest-spyke`, providing an ActiveRecord-like ORM library built on top
|
22
|
+
of `fmrest-core` and [Spyke](https://github.com/balvig/spyke).
|
23
|
+
* `fmrest-core`, providing the core Faraday connection builder, session
|
21
24
|
management, and other core utilities.
|
22
|
-
* `fmrest-spyke`, which provides an ActiveRecord-like ORM library built on top
|
23
|
-
of `fmrest-core` and Spyke.
|
24
25
|
|
25
26
|
## Installation
|
26
27
|
|
@@ -30,7 +31,7 @@ Add this to your Gemfile:
|
|
30
31
|
gem 'fmrest'
|
31
32
|
```
|
32
33
|
|
33
|
-
Or if you just want to use the Faraday connection without the ORM features
|
34
|
+
Or if you just want to use the Faraday connection without the ORM features:
|
34
35
|
|
35
36
|
```ruby
|
36
37
|
gem 'fmrest-core'
|
@@ -40,10 +41,11 @@ gem 'fmrest-core'
|
|
40
41
|
|
41
42
|
### ORM example
|
42
43
|
|
43
|
-
Most people would want to use the ORM features
|
44
|
+
Most people would want to use the ORM features:
|
44
45
|
|
45
46
|
```ruby
|
46
|
-
|
47
|
+
# A Layout model connecting to the "Honeybees Web" FileMaker layout
|
48
|
+
class Honeybee < FmRest::Layout("Honeybees Web")
|
47
49
|
# Connection settings
|
48
50
|
self.fmrest_config = {
|
49
51
|
host: "…",
|
@@ -53,13 +55,28 @@ class Honeybee < FmRest::Spyke::Base
|
|
53
55
|
}
|
54
56
|
|
55
57
|
# Mapped attributes
|
56
|
-
attributes name: "Bee Name", age: "Bee Age"
|
58
|
+
attributes name: "Bee Name", age: "Bee Age", created_on: "Created On"
|
57
59
|
|
58
|
-
#
|
59
|
-
has_portal :
|
60
|
+
# Portal associations
|
61
|
+
has_portal :tasks
|
60
62
|
|
61
|
-
# File
|
63
|
+
# File containers
|
62
64
|
container :photo, field_name: "Bee Photo"
|
65
|
+
|
66
|
+
# Scopes
|
67
|
+
scope :can_legally_fly, -> { query(age: ">18") }
|
68
|
+
|
69
|
+
# Client-side validations
|
70
|
+
validates :name, presence: true
|
71
|
+
|
72
|
+
# Callbacks
|
73
|
+
before_save :set_created_on
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def set_created_on
|
78
|
+
self.created_on = Date.today
|
79
|
+
end
|
63
80
|
end
|
64
81
|
|
65
82
|
# Find a record by id
|
@@ -68,7 +85,7 @@ bee = Honeybee.find(9)
|
|
68
85
|
bee.name = "Hutch"
|
69
86
|
|
70
87
|
# Add a new record to portal
|
71
|
-
bee.
|
88
|
+
bee.tasks.build(urgency: "Today")
|
72
89
|
|
73
90
|
bee.save
|
74
91
|
```
|
@@ -88,11 +105,11 @@ connection = FmRest::V1.build_connection(
|
|
88
105
|
)
|
89
106
|
|
90
107
|
# Get all records (as parsed JSON)
|
91
|
-
connection.get("layouts/
|
108
|
+
connection.get("layouts/FancyLayout/records")
|
92
109
|
|
93
110
|
# Create new record
|
94
111
|
connection.post do |req|
|
95
|
-
req.url "layouts/
|
112
|
+
req.url "layouts/FancyLayout/records"
|
96
113
|
|
97
114
|
# You can just pass a hash for the JSON body
|
98
115
|
req.body = { … }
|
@@ -128,13 +145,14 @@ You can also pass a `:log` option for basic request logging, see the section on
|
|
128
145
|
Option | Description | Format | Default
|
129
146
|
--------------------|--------------------------------------------|-----------------------------|--------
|
130
147
|
`:host` | Hostname with optional port, e.g. `"example.com:9000"` | String | None
|
131
|
-
`:database` |
|
132
|
-
`:username` |
|
133
|
-
`:password` |
|
148
|
+
`:database` | The name of the database to connect to | String | None
|
149
|
+
`:username` | A Data API-ready account | String | None
|
150
|
+
`:password` | Your password | String | None
|
134
151
|
`:account_name` | Alias of `:username` | String | None
|
135
152
|
`:ssl` | SSL options to be forwarded to Faraday | Faraday SSL options | None
|
136
153
|
`:proxy` | Proxy options to be forwarded to Faraday | Faraday proxy options | None
|
137
154
|
`:log` | Log JSON responses to STDOUT | Boolean | `false`
|
155
|
+
`:log_level` | Which log level to log into | Values accepted by `Logger#level=` | `:debug`
|
138
156
|
`:coerce_dates` | See section on [date fields](#date-fields-and-timezones) | Boolean \| `:hybrid` \| `:full` | `false`
|
139
157
|
`:date_format` | Date parsing format | String (FM date format) | `"MM/dd/yyyy"`
|
140
158
|
`:timestamp_format` | Timestmap parsing format | String (FM date format) | `"MM/dd/yyyy HH:mm:ss"`
|
@@ -157,8 +175,8 @@ FmRest.default_connection_settings = {
|
|
157
175
|
}
|
158
176
|
```
|
159
177
|
|
160
|
-
These settings will be used by default by `FmRest::
|
161
|
-
|
178
|
+
These settings will be used by default by `FmRest::Layout` models whenever you
|
179
|
+
don't set `fmrest_config=` explicitly, as well as by
|
162
180
|
`FmRest::V1.build_connection` in case you're setting up your Faraday connection
|
163
181
|
manually.
|
164
182
|
|
@@ -188,11 +206,11 @@ building REST ORM models. fmrest-ruby builds its ORM features atop Spyke,
|
|
188
206
|
bundled in the `fmrest-spyke` gem (already included if you're using the
|
189
207
|
`fmrest` gem).
|
190
208
|
|
191
|
-
To create a model you can inherit directly from `FmRest::
|
192
|
-
|
209
|
+
To create a model you can inherit directly from `FmRest::Layout` (itself a
|
210
|
+
subclass of `Spyke::Base`).
|
193
211
|
|
194
212
|
```ruby
|
195
|
-
class Honeybee < FmRest::
|
213
|
+
class Honeybee < FmRest::Layout
|
196
214
|
end
|
197
215
|
```
|
198
216
|
|
@@ -216,17 +234,23 @@ bee = Honeybee.find(9) # GET request
|
|
216
234
|
|
217
235
|
It's recommended that you read Spyke's documentation for more information on
|
218
236
|
these basic features. If you've used ActiveRecord or similar ORM libraries
|
219
|
-
|
237
|
+
you'll find it quite familiar.
|
238
|
+
|
239
|
+
Notice that `FmRest::Layout` is aliased as `FmRest::Spyke::Base`. Previous
|
240
|
+
versions of fmrest-ruby only provided the latter version, so if you're already
|
241
|
+
using `FmRest::Spyke::Base` there's no need to rename your classes to
|
242
|
+
`FmRest::Layout`, both will continue to work interchangeably.
|
220
243
|
|
221
|
-
In addition, `FmRest::
|
244
|
+
In addition, `FmRest::Layout` extends `Spyke::Base` with the following
|
222
245
|
features:
|
223
246
|
|
224
|
-
###
|
247
|
+
### FmRest::Layout.fmrest_config=
|
225
248
|
|
226
|
-
This allows you to set
|
249
|
+
This allows you to set Data API connection settings specific to your model
|
250
|
+
class:
|
227
251
|
|
228
252
|
```ruby
|
229
|
-
class Honeybee < FmRest::
|
253
|
+
class Honeybee < FmRest::Layout
|
230
254
|
self.fmrest_config = {
|
231
255
|
host: "…",
|
232
256
|
database: "…",
|
@@ -244,9 +268,8 @@ does the initial connection setup and then inherit from it in models using that
|
|
244
268
|
same connection. E.g.:
|
245
269
|
|
246
270
|
```ruby
|
247
|
-
class BeeBase < FmRest::
|
248
|
-
self.fmrest_config = { host: "…", … }
|
249
|
-
}
|
271
|
+
class BeeBase < FmRest::Layout
|
272
|
+
self.fmrest_config = { host: "…", database: "…", … }
|
250
273
|
end
|
251
274
|
|
252
275
|
class Honeybee < BeeBase
|
@@ -254,34 +277,46 @@ class Honeybee < BeeBase
|
|
254
277
|
end
|
255
278
|
```
|
256
279
|
|
280
|
+
Also, if not set, your model will try to use
|
281
|
+
`FmRest.default_connection_settings` instead.
|
282
|
+
|
257
283
|
#### Connection settings overlays
|
258
284
|
|
259
285
|
There may be cases where you want to use a different set of connection settings
|
260
286
|
depending on context. For example, if you want to use username and password
|
261
|
-
provided by the user in a web application. Since
|
262
|
-
at the class level, changing the username/password for the model in one
|
263
|
-
would also change it in all other contexts, leading to security issues.
|
287
|
+
provided by the user in a web application. Since `.fmrest_config`
|
288
|
+
is set at the class level, changing the username/password for the model in one
|
289
|
+
context would also change it in all other contexts, leading to security issues.
|
264
290
|
|
265
291
|
To solve this scenario, fmrest-ruby provides a way of defining thread-local and
|
266
|
-
reversible connection settings overlays through
|
292
|
+
reversible connection settings overlays through
|
293
|
+
`.fmrest_config_overlay=`.
|
267
294
|
|
268
295
|
See the [main document on connection setting overlays](docs/ConfigOverlays.md)
|
269
296
|
for details on how it works.
|
270
297
|
|
271
|
-
###
|
298
|
+
### FmRest::Layout.layout
|
272
299
|
|
273
|
-
Use `
|
300
|
+
Use `layout` to set the layout name for your model.
|
274
301
|
|
275
302
|
```ruby
|
276
|
-
class Honeybee < FmRest::
|
303
|
+
class Honeybee < FmRest::Layout
|
277
304
|
layout "Honeybees Web"
|
278
305
|
end
|
279
306
|
```
|
280
307
|
|
281
|
-
|
282
|
-
the layout
|
308
|
+
Alternatively, if you're inheriting from `FmRest::Layout` directly you can set
|
309
|
+
the layout name in the class definition line:
|
283
310
|
|
284
|
-
|
311
|
+
```ruby
|
312
|
+
class Honeybee < FmRest::Layout("Honeybees Web")
|
313
|
+
```
|
314
|
+
|
315
|
+
Note that you only need to manually set the layout name if the name of the
|
316
|
+
class and the name of the layout differ, otherwise fmrest-ruby will just use
|
317
|
+
the name of the class.
|
318
|
+
|
319
|
+
### FmRest::Layout.request_auth_token
|
285
320
|
|
286
321
|
Requests a Data API session token using the connection settings in
|
287
322
|
`fmrest_config` and returns it if successful, otherwise returns `false`.
|
@@ -290,16 +325,16 @@ You normally don't need to use this method as fmrest-ruby will automatically
|
|
290
325
|
request and store session tokens for you (provided that `:autologin` is
|
291
326
|
`true`).
|
292
327
|
|
293
|
-
###
|
328
|
+
### FmRest::Layout.logout
|
294
329
|
|
295
|
-
Use
|
330
|
+
Use `.logout` to log out from the database session (you may call it on any
|
296
331
|
model that uses the database session you want to log out from).
|
297
332
|
|
298
333
|
```ruby
|
299
334
|
Honeybee.logout
|
300
335
|
```
|
301
336
|
|
302
|
-
### Mapped
|
337
|
+
### Mapped FmRest::Layout.attributes
|
303
338
|
|
304
339
|
Spyke allows you to define your model's attributes using `attributes`, however
|
305
340
|
sometimes FileMaker's field names aren't very Ruby-ORM-friendly, especially
|
@@ -308,7 +343,7 @@ fmrest-ruby extends `attributes`' functionality to allow you to map
|
|
308
343
|
Ruby-friendly attribute names to FileMaker field names. E.g.:
|
309
344
|
|
310
345
|
```ruby
|
311
|
-
class Honeybee < FmRest::
|
346
|
+
class Honeybee < FmRest::Layout
|
312
347
|
attributes first_name: "First Name", last_name: "Last Name"
|
313
348
|
end
|
314
349
|
```
|
@@ -327,16 +362,16 @@ bee.first_name = "Queen"
|
|
327
362
|
bee.attributes # => { "First Name": "Queen", "Last Name": "Buzz" }
|
328
363
|
```
|
329
364
|
|
330
|
-
###
|
365
|
+
### FmRest::Layout.has_portal
|
331
366
|
|
332
367
|
You can define portal associations on your model wth `has_portal`, as such:
|
333
368
|
|
334
369
|
```ruby
|
335
|
-
class Honeybee < FmRest::
|
370
|
+
class Honeybee < FmRest::Layout
|
336
371
|
has_portal :flowers
|
337
372
|
end
|
338
373
|
|
339
|
-
class Flower < FmRest::
|
374
|
+
class Flower < FmRest::Layout
|
340
375
|
attributes :color, :species
|
341
376
|
end
|
342
377
|
```
|
@@ -371,8 +406,8 @@ Guides](https://guides.rubyonrails.org/active_model_basics.html#dirty).
|
|
371
406
|
Since Spyke is API-agnostic it only provides a wide-purpose `.where` method for
|
372
407
|
passing arbitrary parameters to the REST backend. fmrest-ruby however is well
|
373
408
|
aware of its backend API, so it extends Spkye models with a bunch of useful
|
374
|
-
querying methods: `.query`, `.
|
375
|
-
etc.
|
409
|
+
querying methods: `.query`, `.match`, `.omit`, `.limit`, `.offset`, `.sort`,
|
410
|
+
`.portal`, `.script`, etc.
|
376
411
|
|
377
412
|
See the [main document on querying](docs/Querying.md) for detailed information
|
378
413
|
on the query API methods.
|
@@ -393,7 +428,7 @@ detailed information on how those work.
|
|
393
428
|
You can define container fields on your model class with `container`:
|
394
429
|
|
395
430
|
```ruby
|
396
|
-
class Honeybee < FmRest::
|
431
|
+
class Honeybee < FmRest::Layout
|
397
432
|
container :photo, field_name: "Beehive Photo ID"
|
398
433
|
end
|
399
434
|
```
|
@@ -411,7 +446,7 @@ details.
|
|
411
446
|
|
412
447
|
### Setting global field values
|
413
448
|
|
414
|
-
You can call `.set_globals` on any `FmRest::
|
449
|
+
You can call `.set_globals` on any `FmRest::Layout` model to set global
|
415
450
|
field values on the database that model is configured for.
|
416
451
|
|
417
452
|
See the [main document on setting global field values](docs/GlobalFields.md)
|
@@ -423,7 +458,7 @@ If using `fmrest-spyke` with Rails then pretty log output will be set up for
|
|
423
458
|
you automatically by Spyke (see [their
|
424
459
|
README](https://github.com/balvig/spyke#log-output)).
|
425
460
|
|
426
|
-
You can also enable simple Faraday
|
461
|
+
You can also enable simple Faraday logging of raw requests (useful for
|
427
462
|
debugging) by passing `log: true` in the options hash for either
|
428
463
|
`FmRest.default_connection_settings=` or your models' `fmrest_config=`, e.g.:
|
429
464
|
|
@@ -435,7 +470,7 @@ FmRest.default_connection_settings = {
|
|
435
470
|
}
|
436
471
|
|
437
472
|
# Or in your model
|
438
|
-
class LoggyBee < FmRest::
|
473
|
+
class LoggyBee < FmRest::Layout
|
439
474
|
self.fmrest_config = {
|
440
475
|
host: "…",
|
441
476
|
…
|
@@ -444,12 +479,22 @@ class LoggyBee < FmRest::Spyke::Base
|
|
444
479
|
end
|
445
480
|
```
|
446
481
|
|
447
|
-
|
482
|
+
You can also pass `log_level` to connection settings to change the severity of
|
483
|
+
log output (defaults to `:debug`).
|
484
|
+
|
485
|
+
By default fmrest-ruby logs to STDOUT or to Rails' logger object if available.
|
486
|
+
You can change this by providing your own logger object to `FmRest.logger=`:
|
487
|
+
|
488
|
+
```ruby
|
489
|
+
FmRest.logger = Logger.new("fmrest.log")
|
490
|
+
```
|
491
|
+
|
492
|
+
If you need to set up more complex logging for your models you can use the
|
448
493
|
`faraday` block inside your class to inject your own logger middleware into the
|
449
494
|
Faraday connection, e.g.:
|
450
495
|
|
451
496
|
```ruby
|
452
|
-
class LoggyBee < FmRest::
|
497
|
+
class LoggyBee < FmRest::Layout
|
453
498
|
faraday do |conn|
|
454
499
|
conn.response :logger, MyApp.logger, bodies: true
|
455
500
|
end
|
@@ -460,31 +505,31 @@ end
|
|
460
505
|
|
461
506
|
FM Data API reference: https://fmhelp.filemaker.com/docs/18/en/dataapi/
|
462
507
|
|
463
|
-
| FM 18 Data API feature | Supported by basic connection | Supported by FmRest::
|
464
|
-
|
465
|
-
| Log in using HTTP Basic Auth | Yes | Yes
|
466
|
-
| Log in using OAuth | No | No
|
467
|
-
| Log in to an external data source | No | No
|
468
|
-
| Log in using a FileMaker ID account | No | No
|
469
|
-
| Log out | Yes | Yes
|
470
|
-
| Get product information | Manual* | No
|
471
|
-
| Get database names | Manual* | No
|
472
|
-
| Get script names | Manual* | No
|
473
|
-
| Get layout names | Manual* | No
|
474
|
-
| Get layout metadata | Manual* | No
|
475
|
-
| Create a record | Manual* | Yes
|
476
|
-
| Edit a record | Manual* | Yes
|
477
|
-
| Duplicate a record | Manual* | No
|
478
|
-
| Delete a record | Manual* | Yes
|
479
|
-
| Edit portal records | Manual* | Yes
|
480
|
-
| Get a single record | Manual* | Yes
|
481
|
-
| Get a range of records | Manual* | Yes
|
482
|
-
| Get container data | Manual* | Yes
|
483
|
-
| Upload container data | Manual* | Yes
|
484
|
-
| Perform a find request | Manual* | Yes
|
485
|
-
| Set global field values | Manual* | Yes
|
486
|
-
| Run a script | Manual* | Yes
|
487
|
-
| Run a script with another request | Manual* | Yes
|
508
|
+
| FM 18 Data API feature | Supported by basic connection | Supported by FmRest::Layout |
|
509
|
+
|-------------------------------------|-------------------------------|-----------------------------|
|
510
|
+
| Log in using HTTP Basic Auth | Yes | Yes |
|
511
|
+
| Log in using OAuth | No | No |
|
512
|
+
| Log in to an external data source | No | No |
|
513
|
+
| Log in using a FileMaker ID account | No | No |
|
514
|
+
| Log out | Yes | Yes |
|
515
|
+
| Get product information | Manual* | No |
|
516
|
+
| Get database names | Manual* | No |
|
517
|
+
| Get script names | Manual* | No |
|
518
|
+
| Get layout names | Manual* | No |
|
519
|
+
| Get layout metadata | Manual* | No |
|
520
|
+
| Create a record | Manual* | Yes |
|
521
|
+
| Edit a record | Manual* | Yes |
|
522
|
+
| Duplicate a record | Manual* | No |
|
523
|
+
| Delete a record | Manual* | Yes |
|
524
|
+
| Edit portal records | Manual* | Yes |
|
525
|
+
| Get a single record | Manual* | Yes |
|
526
|
+
| Get a range of records | Manual* | Yes |
|
527
|
+
| Get container data | Manual* | Yes |
|
528
|
+
| Upload container data | Manual* | Yes |
|
529
|
+
| Perform a find request | Manual* | Yes |
|
530
|
+
| Set global field values | Manual* | Yes |
|
531
|
+
| Run a script | Manual* | Yes |
|
532
|
+
| Run a script with another request | Manual* | Yes |
|
488
533
|
|
489
534
|
\* You can manually supply the URL and JSON to a `FmRest` connection.
|
490
535
|
|
@@ -519,6 +564,6 @@ See [LICENSE.txt](LICENSE.txt).
|
|
519
564
|
|
520
565
|
## Disclaimer
|
521
566
|
|
522
|
-
This project is not sponsored by or otherwise affiliated with
|
523
|
-
an Apple subsidiary. FileMaker is a trademark of
|
524
|
-
the U.S. and other countries.
|
567
|
+
This project is not sponsored by or otherwise affiliated with Claris
|
568
|
+
International Inc., an Apple Inc. subsidiary. FileMaker is a trademark of
|
569
|
+
Claris International Inc., registered in the U.S. and other countries.
|
data/lib/fmrest/spyke.rb
CHANGED
@@ -1,12 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
require "spyke"
|
5
|
-
rescue LoadError => e
|
6
|
-
e.message << " (Did you include Spyke in your Gemfile?)" unless e.message.frozen?
|
7
|
-
raise e
|
8
|
-
end
|
9
|
-
|
3
|
+
require "spyke"
|
10
4
|
require "fmrest"
|
11
5
|
require "fmrest/spyke/spyke_formatter"
|
12
6
|
require "fmrest/spyke/model"
|
data/lib/fmrest/spyke/base.rb
CHANGED
@@ -6,4 +6,19 @@ module FmRest
|
|
6
6
|
include FmRest::Spyke::Model
|
7
7
|
end
|
8
8
|
end
|
9
|
+
|
10
|
+
Layout = Spyke::Base
|
11
|
+
|
12
|
+
# Shortcut for creating a Layout class and setting its FM layout name.
|
13
|
+
#
|
14
|
+
# @param layout [String] The FM layout to connect this class to
|
15
|
+
#
|
16
|
+
# @return [Class] A new subclass of `FmRest::Layout` with the FM layout
|
17
|
+
# setting already set.
|
18
|
+
#
|
19
|
+
def self.Layout(layout)
|
20
|
+
Class.new(Layout) do
|
21
|
+
self.layout layout
|
22
|
+
end
|
23
|
+
end
|
9
24
|
end
|
@@ -26,10 +26,10 @@ module FmRest
|
|
26
26
|
|
27
27
|
class_methods do
|
28
28
|
# Methods delegated to `FmRest::Spyke::Relation`
|
29
|
-
delegate :limit, :offset, :sort, :order, :query, :
|
30
|
-
|
31
|
-
|
32
|
-
|
29
|
+
delegate :limit, :offset, :sort, :order, :query, :match, :omit,
|
30
|
+
:portal, :portals, :includes, :with_all_portals, :without_portals,
|
31
|
+
:script, :find_one, :first, :any, :find_some, :find_in_batches,
|
32
|
+
:find_each, to: :all
|
33
33
|
|
34
34
|
# Spyke override -- Use FmRest's Relation instead of Spyke's vanilla
|
35
35
|
# one
|
@@ -4,9 +4,6 @@ module FmRest
|
|
4
4
|
module Spyke
|
5
5
|
module Model
|
6
6
|
module Serialization
|
7
|
-
FM_DATE_FORMAT = "%m/%d/%Y"
|
8
|
-
FM_DATETIME_FORMAT = "#{FM_DATE_FORMAT} %H:%M:%S"
|
9
|
-
|
10
7
|
# Spyke override -- Return FM Data API's expected JSON format,
|
11
8
|
# including only modified fields.
|
12
9
|
#
|
@@ -76,13 +73,21 @@ module FmRest
|
|
76
73
|
# Modifies the given hash in-place encoding non-string values (e.g.
|
77
74
|
# dates) to their string representation when appropriate.
|
78
75
|
#
|
76
|
+
# nil gets converted to empty string as the Data API doesn't accept
|
77
|
+
# nulls in JSON. Likewise, true and false get converted to 1 and 0
|
78
|
+
# respectively.
|
79
|
+
#
|
79
80
|
def serialize_values!(params)
|
80
81
|
params.transform_values! do |value|
|
81
82
|
case value
|
82
|
-
when *datetime_classes
|
83
|
-
convert_datetime_timezone(value.to_datetime).strftime(FM_DATETIME_FORMAT)
|
84
|
-
when *date_classes
|
85
|
-
value.strftime(FM_DATE_FORMAT)
|
83
|
+
when *FmRest::V1.datetime_classes
|
84
|
+
FmRest::V1.convert_datetime_timezone(value.to_datetime, fmrest_config.timezone).strftime(FmRest::V1::Dates::FM_DATETIME_FORMAT)
|
85
|
+
when *FmRest::V1.date_classes
|
86
|
+
value.strftime(FmRest::V1::Dates::FM_DATE_FORMAT)
|
87
|
+
when nil
|
88
|
+
""
|
89
|
+
when true, false
|
90
|
+
value ? 1 : 0
|
86
91
|
else
|
87
92
|
value
|
88
93
|
end
|
@@ -90,25 +95,6 @@ module FmRest
|
|
90
95
|
|
91
96
|
params
|
92
97
|
end
|
93
|
-
|
94
|
-
def convert_datetime_timezone(dt)
|
95
|
-
case fmrest_config.timezone
|
96
|
-
when :utc, "utc"
|
97
|
-
dt.new_offset(0)
|
98
|
-
when :local, "local"
|
99
|
-
dt.new_offset(FmRest::V1.local_offset_for_datetime(dt))
|
100
|
-
when nil
|
101
|
-
dt
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
def datetime_classes
|
106
|
-
[DateTime, Time, defined?(FmRest::StringDateTime) && FmRest::StringDateTime].compact
|
107
|
-
end
|
108
|
-
|
109
|
-
def date_classes
|
110
|
-
[Date, defined?(FmRest::StringDate) && FmRest::StringDate].compact
|
111
|
-
end
|
112
98
|
end
|
113
99
|
end
|
114
100
|
end
|
@@ -6,12 +6,26 @@ module FmRest
|
|
6
6
|
module URI
|
7
7
|
extend ::ActiveSupport::Concern
|
8
8
|
|
9
|
+
included do
|
10
|
+
# Make the layout setting inheritable
|
11
|
+
class_attribute :_layout, instance_predicate: false
|
12
|
+
|
13
|
+
class << self
|
14
|
+
protected :_layout
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
9
18
|
class_methods do
|
10
|
-
# Accessor for FM layout (
|
19
|
+
# Accessor for FM layout (user for building request URIs).
|
20
|
+
#
|
21
|
+
# @param layout [String] The FM layout to connect this class to
|
22
|
+
#
|
23
|
+
# @return [String] The current layout if manually set, or the name of
|
24
|
+
# the class otherwise
|
11
25
|
#
|
12
26
|
def layout(layout = nil)
|
13
|
-
|
14
|
-
|
27
|
+
self._layout = layout.dup.to_s.freeze if layout
|
28
|
+
self._layout || model_name.name
|
15
29
|
end
|
16
30
|
|
17
31
|
# Spyke override -- Extends `uri` to default to FM Data's URI schema
|
@@ -5,6 +5,8 @@ module FmRest
|
|
5
5
|
class Relation < ::Spyke::Relation
|
6
6
|
SORT_PARAM_MATCHER = /(.*?)(!|__desc(?:end)?)?\Z/.freeze
|
7
7
|
|
8
|
+
class UnknownQueryKey < ArgumentError; end
|
9
|
+
|
8
10
|
# NOTE: We need to keep limit, offset, sort, query and portal accessors
|
9
11
|
# separate from regular params because FM Data API uses either "limit" or
|
10
12
|
# "_limit" (or "_offset", etc.) as param keys depending on the type of
|
@@ -12,7 +14,7 @@ module FmRest
|
|
12
14
|
|
13
15
|
|
14
16
|
attr_accessor :limit_value, :offset_value, :sort_params, :query_params,
|
15
|
-
:included_portals, :portal_params, :script_params
|
17
|
+
:or_flag, :included_portals, :portal_params, :script_params
|
16
18
|
|
17
19
|
def initialize(*_args)
|
18
20
|
super
|
@@ -161,16 +163,111 @@ module FmRest
|
|
161
163
|
portal(false)
|
162
164
|
end
|
163
165
|
|
166
|
+
# Sets conditions for a find request. Conditions must be given in
|
167
|
+
# `{ field: condition }` format, where `condition` is normally a string
|
168
|
+
# sent raw to the Data API server, so you can use FileMaker find
|
169
|
+
# operators. You can also pass Ruby range or date/datetime objects for
|
170
|
+
# condition values, and they'll be converted to the appropriate Data API
|
171
|
+
# representation.
|
172
|
+
#
|
173
|
+
# Passing `omit: true` in a conditions set will negate all conditions in
|
174
|
+
# that set.
|
175
|
+
#
|
176
|
+
# You can modify the way conditions are added (i.e. through logical AND
|
177
|
+
# or OR) by pre-chaining `.or`. By default it adds conditions through
|
178
|
+
# logical AND.
|
179
|
+
#
|
180
|
+
# Note that because of the way the Data API works, logical AND conditions
|
181
|
+
# on a single field are not possible. Because of that, if you try to set
|
182
|
+
# two AND conditions for the same field, the previously existing one will
|
183
|
+
# be overwritten with the new condition.
|
184
|
+
#
|
185
|
+
# It is recommended that you learn how the Data API represents conditions
|
186
|
+
# in its find requests (i.e. an array of JSON objects with conditions on
|
187
|
+
# fields). This method internally uses that same representation, which
|
188
|
+
# you can view by inspecting the resulting relations. Understanding that
|
189
|
+
# representation will also make the limitations of this Ruby API clear.
|
190
|
+
#
|
191
|
+
# @example
|
192
|
+
# Person.query(name: "=Alice") # Simple query
|
193
|
+
# Person.query(age: (20..29)) # Query using a Ruby range
|
194
|
+
# Person.query(created_on: Date.today..Date.today-1)
|
195
|
+
# Person.query(name: "=Alice", age: ">20") # Query multiple fields (logical AND)
|
196
|
+
# Person.query(name: "=Alice").query(age: ">20") # Same effect as above example
|
197
|
+
# Person.query(name: "=Bob", omit: true) # Negate a query (i.e. find people not named Bob)
|
198
|
+
# Person.query(pets: { name: "=Snuggles" }) # Query portal fields
|
199
|
+
# Person.query({ name: "=Alice" }, { name: "=Bob" }) # Separate conditions through logical OR
|
200
|
+
# Person.query(name: "=Alice").or.query(name: "=Bob") # Same effect as above example
|
201
|
+
# @return [FmRest::Spyke::Relation] a new relation with the given find
|
202
|
+
# conditions applied
|
164
203
|
def query(*params)
|
165
204
|
with_clone do |r|
|
166
|
-
|
205
|
+
params = params.flatten.map { |p| normalize_query_params(p) }
|
206
|
+
|
207
|
+
if r.or_flag || r.query_params.empty?
|
208
|
+
r.query_params += params
|
209
|
+
r.or_flag = nil
|
210
|
+
elsif params.length > r.query_params.length
|
211
|
+
params[0, r.query_params.length].each_with_index do |p, i|
|
212
|
+
r.query_params[i].merge!(p)
|
213
|
+
end
|
214
|
+
|
215
|
+
remainder = params.length - r.query_params.length
|
216
|
+
r.query_params += params[-remainder, remainder]
|
217
|
+
else
|
218
|
+
params.each_with_index { |p, i| r.query_params[i].merge!(p) }
|
219
|
+
end
|
167
220
|
end
|
168
221
|
end
|
169
222
|
|
223
|
+
# Similar to `.query`, but sets exact string match queries (i.e.
|
224
|
+
# prefixes queries with ==) and escapes find operators in the given
|
225
|
+
# queries using `FmRest.e`.
|
226
|
+
#
|
227
|
+
# @example
|
228
|
+
# Person.query(email: "bob@example.com") # Find exact email
|
229
|
+
# @return [FmRest::Spyke::Relation] a new relation with the exact match
|
230
|
+
# conditions applied
|
231
|
+
def match(*params)
|
232
|
+
query(transform_query_values(params) { |v| "==#{FmRest::V1.escape_find_operators(v)}" })
|
233
|
+
end
|
234
|
+
|
235
|
+
# Negated version of `.query`, sets conditions to omit in a find request.
|
236
|
+
#
|
237
|
+
# This is the same as passing `omit: true` to `.query`.
|
238
|
+
#
|
239
|
+
# @return [FmRest::Spyke::Relation] a new relation with the given find
|
240
|
+
# conditions applied negated
|
170
241
|
def omit(params)
|
171
242
|
query(params.merge(omit: true))
|
172
243
|
end
|
173
244
|
|
245
|
+
# Signals that the next query conditions to be set (through `.query`,
|
246
|
+
# `.match`, etc.) should be added as a logical OR relative to previously
|
247
|
+
# set conditions (rather than the default AND).
|
248
|
+
#
|
249
|
+
# In practice this means the JSON query request will have a new
|
250
|
+
# conditions object appended, e.g.:
|
251
|
+
#
|
252
|
+
# ```
|
253
|
+
# {"query": [{"field": "condition"}, {"field": "OR-added condition"}]}
|
254
|
+
# ```
|
255
|
+
#
|
256
|
+
# You can call this method with or without parameters. If parameters are
|
257
|
+
# given they will be passed down to `.query` (and those conditions
|
258
|
+
# immediately set), otherwise it just prepares the next
|
259
|
+
# conditions-setting method to use OR.
|
260
|
+
#
|
261
|
+
# @example
|
262
|
+
# # Add conditions directly in .or call:
|
263
|
+
# Person.query(name: "=Alice").or(name: "=Bob")
|
264
|
+
# # Add exact match conditions through method chaining
|
265
|
+
# Person.match(email: "alice@example.com").or.match(email: "bob@example.com")
|
266
|
+
def or(*params)
|
267
|
+
clone = with_clone { |r| r.or_flag = true }
|
268
|
+
params.empty? ? clone : clone.query(*params)
|
269
|
+
end
|
270
|
+
|
174
271
|
# @return [Boolean] whether a query was set on this relation
|
175
272
|
def has_query?
|
176
273
|
query_params.present?
|
@@ -328,11 +425,91 @@ module FmRest
|
|
328
425
|
next
|
329
426
|
end
|
330
427
|
|
331
|
-
#
|
332
|
-
if
|
333
|
-
|
428
|
+
# Portal fields query (nested hash), e.g. { contact: { name: "Hutch" } }
|
429
|
+
if v.kind_of?(Hash)
|
430
|
+
if k.kind_of?(Symbol)
|
431
|
+
portal_key, = klass.portal_options.find { |_, opts| opts[:name].to_s == k.to_s }
|
432
|
+
|
433
|
+
if portal_key
|
434
|
+
portal_model = klass.associations[k].klass
|
435
|
+
|
436
|
+
portal_normalized = v.each_with_object({}) do |(pk, pv), h|
|
437
|
+
normalize_single_query_param_for_model(portal_model, pk, pv, h)
|
438
|
+
end
|
439
|
+
|
440
|
+
normalized.merge!(portal_normalized.transform_keys { |pf| "#{portal_key}::#{pf}" })
|
441
|
+
else
|
442
|
+
raise UnknownQueryKey, "No portal matches the query key `:#{k}` on #{klass.name}. If you are trying to use the literal string '#{k}' pass it as a string instead of a symbol."
|
443
|
+
end
|
444
|
+
else
|
445
|
+
normalized.merge!(v.transform_keys { |pf| "#{k}::#{pf}" })
|
446
|
+
end
|
447
|
+
|
448
|
+
next
|
449
|
+
end
|
450
|
+
|
451
|
+
# Attribute query (scalar values), e.g. { name: "Hutch" }
|
452
|
+
normalize_single_query_param_for_model(klass, k, v, normalized)
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
456
|
+
def normalize_single_query_param_for_model(model, k, v, hash)
|
457
|
+
if k.kind_of?(Symbol)
|
458
|
+
if model.mapped_attributes.has_key?(k)
|
459
|
+
hash[model.mapped_attributes[k].to_s] = format_query_condition(v)
|
460
|
+
else
|
461
|
+
raise UnknownQueryKey, "No attribute matches the query key `:#{k}` on #{model.name}. If you are trying to use the literal string '#{k}' pass it as a string instead of a symbol."
|
462
|
+
end
|
463
|
+
else
|
464
|
+
hash[k.to_s] = format_query_condition(v)
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
# Transforms various Ruby data types to FileMaker search condition
|
469
|
+
# strings
|
470
|
+
#
|
471
|
+
def format_query_condition(condition)
|
472
|
+
case condition
|
473
|
+
when nil
|
474
|
+
"=" # Search for empty field
|
475
|
+
when Range
|
476
|
+
format_range_condition(condition)
|
477
|
+
when *FmRest::V1.datetime_classes
|
478
|
+
FmRest::V1.convert_datetime_timezone(condition.to_datetime, klass.fmrest_config.timezone)
|
479
|
+
.strftime(FmRest::V1::Dates::FM_DATETIME_FORMAT)
|
480
|
+
when *FmRest::V1.date_classes
|
481
|
+
condition.strftime(FmRest::V1::Dates::FM_DATE_FORMAT)
|
482
|
+
else
|
483
|
+
condition
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
def format_range_condition(range)
|
488
|
+
if range.first.kind_of?(Numeric)
|
489
|
+
if range.first == Float::INFINITY || range.end == -Float::INFINITY
|
490
|
+
raise ArgumentError, "Can't search for a range that begins at +Infinity or ends at -Infinity"
|
491
|
+
elsif range.first == -Float::INFINITY
|
492
|
+
if range.end == Float::INFINITY || range.end.nil?
|
493
|
+
"*" # Search for non-empty field
|
494
|
+
else
|
495
|
+
range.exclude_end? ? "<#{range.end}" : "<=#{range.end}"
|
496
|
+
end
|
497
|
+
elsif range.end == Float::INFINITY || range.end.nil?
|
498
|
+
">=#{range.first}"
|
499
|
+
elsif range.exclude_end? && range.last.respond_to?(:pred)
|
500
|
+
"#{range.first}..#{range.last.pred}"
|
334
501
|
else
|
335
|
-
|
502
|
+
"#{range.first}..#{range.last}"
|
503
|
+
end
|
504
|
+
else
|
505
|
+
"#{format_query_condition(range.first)}..#{format_query_condition(range.last)}"
|
506
|
+
end
|
507
|
+
end
|
508
|
+
|
509
|
+
def transform_query_values(*params, &block)
|
510
|
+
params.flatten.map do |p|
|
511
|
+
p.transform_values do |v|
|
512
|
+
v.kind_of?(Hash) ? v.transform_values(&block) : yield(v)
|
336
513
|
end
|
337
514
|
end
|
338
515
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fmrest-spyke
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.16.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Pedro Carbajal
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-04-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: fmrest-core
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - '='
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.
|
19
|
+
version: 0.16.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - '='
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 0.
|
26
|
+
version: 0.16.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: spyke
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|