fmrest-spyke 0.13.0 → 0.15.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 +4 -4
- data/.yardopts +1 -0
- data/CHANGELOG.md +20 -0
- data/README.md +116 -81
- 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 +4 -26
- data/lib/fmrest/spyke/model/uri.rb +17 -3
- data/lib/fmrest/spyke/relation.rb +183 -6
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a23ace303897e8ce4f81635a6b800aae8f4cba61f79bc9594ba43669ad900fd8
|
4
|
+
data.tar.gz: 117e2ec52a4fdbfec79ec846160b8bda7a5a4a43ea0059de6a1557913efe7182
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9b77e8a541883d8f0744653f69443c94eaf6e8fe40637107329b579791605c20086b09d79dd62e807b1bd4e9a941e998ddcac11fcac7b864908b7bfd613f88ae
|
7
|
+
data.tar.gz: fb90f1df6a5287bebe5ac77c9e88c6548c6101bdd29cbb4f2b0c9382a0d4e1781dbd717b504bc88bdc14b07770568cdc42871a9a65c90fbfeed545485a7480ca
|
data/.yardopts
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,25 @@
|
|
1
1
|
## Changelog
|
2
2
|
|
3
|
+
### 0.15.2
|
4
|
+
|
5
|
+
* Fix autoloading of `FmRest::Layout`
|
6
|
+
|
7
|
+
### 0.15.0
|
8
|
+
|
9
|
+
* Much improved querying API (see documentation on querying), adding new
|
10
|
+
`.query` capabilities, as well as two new methods: `.match` and `.or`
|
11
|
+
|
12
|
+
### 0.14.0
|
13
|
+
|
14
|
+
* Aliased `FmRest::Spyke::Base` as `FmRest::Layout` (now preferred), and
|
15
|
+
provided a shortcut version for setting the layout name (e.g. `class Foo <
|
16
|
+
FmRest::Layout("LayoutName")`)
|
17
|
+
* Made `layout` class setting subclass-inheritable
|
18
|
+
|
19
|
+
### 0.13.1
|
20
|
+
|
21
|
+
* Fix downloading of container field data from FMS19+
|
22
|
+
|
3
23
|
### 0.13.0
|
4
24
|
|
5
25
|
* Split `fmrest` gem into `fmrest-core` and `fmrest-spyke`. `fmrest` becomes a
|
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,9 +145,9 @@ 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
|
@@ -157,8 +174,8 @@ FmRest.default_connection_settings = {
|
|
157
174
|
}
|
158
175
|
```
|
159
176
|
|
160
|
-
These settings will be used by default by `FmRest::
|
161
|
-
|
177
|
+
These settings will be used by default by `FmRest::Layout` models whenever you
|
178
|
+
don't set `fmrest_config=` explicitly, as well as by
|
162
179
|
`FmRest::V1.build_connection` in case you're setting up your Faraday connection
|
163
180
|
manually.
|
164
181
|
|
@@ -188,11 +205,11 @@ building REST ORM models. fmrest-ruby builds its ORM features atop Spyke,
|
|
188
205
|
bundled in the `fmrest-spyke` gem (already included if you're using the
|
189
206
|
`fmrest` gem).
|
190
207
|
|
191
|
-
To create a model you can inherit directly from `FmRest::
|
192
|
-
|
208
|
+
To create a model you can inherit directly from `FmRest::Layout` (itself a
|
209
|
+
subclass of `Spyke::Base`).
|
193
210
|
|
194
211
|
```ruby
|
195
|
-
class Honeybee < FmRest::
|
212
|
+
class Honeybee < FmRest::Layout
|
196
213
|
end
|
197
214
|
```
|
198
215
|
|
@@ -216,17 +233,23 @@ bee = Honeybee.find(9) # GET request
|
|
216
233
|
|
217
234
|
It's recommended that you read Spyke's documentation for more information on
|
218
235
|
these basic features. If you've used ActiveRecord or similar ORM libraries
|
219
|
-
|
236
|
+
you'll find it quite familiar.
|
220
237
|
|
221
|
-
|
238
|
+
Notice that `FmRest::Layout` is aliased as `FmRest::Spyke::Base`. Previous
|
239
|
+
versions of fmrest-ruby only provided the latter version, so if you're already
|
240
|
+
using `FmRest::Spyke::Base` there's no need to rename your classes to
|
241
|
+
`FmRest::Layout`, both will continue to work interchangeably.
|
242
|
+
|
243
|
+
In addition, `FmRest::Layout` extends `Spyke::Base` with the following
|
222
244
|
features:
|
223
245
|
|
224
|
-
###
|
246
|
+
### FmRest::Layout.fmrest_config=
|
225
247
|
|
226
|
-
This allows you to set
|
248
|
+
This allows you to set Data API connection settings specific to your model
|
249
|
+
class:
|
227
250
|
|
228
251
|
```ruby
|
229
|
-
class Honeybee < FmRest::
|
252
|
+
class Honeybee < FmRest::Layout
|
230
253
|
self.fmrest_config = {
|
231
254
|
host: "…",
|
232
255
|
database: "…",
|
@@ -244,9 +267,8 @@ does the initial connection setup and then inherit from it in models using that
|
|
244
267
|
same connection. E.g.:
|
245
268
|
|
246
269
|
```ruby
|
247
|
-
class BeeBase < FmRest::
|
248
|
-
self.fmrest_config = { host: "…", … }
|
249
|
-
}
|
270
|
+
class BeeBase < FmRest::Layout
|
271
|
+
self.fmrest_config = { host: "…", database: "…", … }
|
250
272
|
end
|
251
273
|
|
252
274
|
class Honeybee < BeeBase
|
@@ -254,34 +276,46 @@ class Honeybee < BeeBase
|
|
254
276
|
end
|
255
277
|
```
|
256
278
|
|
279
|
+
Also, if not set, your model will try to use
|
280
|
+
`FmRest.default_connection_settings` instead.
|
281
|
+
|
257
282
|
#### Connection settings overlays
|
258
283
|
|
259
284
|
There may be cases where you want to use a different set of connection settings
|
260
285
|
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.
|
286
|
+
provided by the user in a web application. Since `.fmrest_config`
|
287
|
+
is set at the class level, changing the username/password for the model in one
|
288
|
+
context would also change it in all other contexts, leading to security issues.
|
264
289
|
|
265
290
|
To solve this scenario, fmrest-ruby provides a way of defining thread-local and
|
266
|
-
reversible connection settings overlays through
|
291
|
+
reversible connection settings overlays through
|
292
|
+
`.fmrest_config_overlay=`.
|
267
293
|
|
268
294
|
See the [main document on connection setting overlays](docs/ConfigOverlays.md)
|
269
295
|
for details on how it works.
|
270
296
|
|
271
|
-
###
|
297
|
+
### FmRest::Layout.layout
|
272
298
|
|
273
|
-
Use `
|
299
|
+
Use `layout` to set the layout name for your model.
|
274
300
|
|
275
301
|
```ruby
|
276
|
-
class Honeybee < FmRest::
|
302
|
+
class Honeybee < FmRest::Layout
|
277
303
|
layout "Honeybees Web"
|
278
304
|
end
|
279
305
|
```
|
280
306
|
|
281
|
-
|
282
|
-
the layout
|
307
|
+
Alternatively, if you're inheriting from `FmRest::Layout` directly you can set
|
308
|
+
the layout name in the class definition line:
|
309
|
+
|
310
|
+
```ruby
|
311
|
+
class Honeybee < FmRest::Layout("Honeybees Web")
|
312
|
+
```
|
313
|
+
|
314
|
+
Note that you only need to manually set the layout name if the name of the
|
315
|
+
class and the name of the layout differ, otherwise fmrest-ruby will just use
|
316
|
+
the name of the class.
|
283
317
|
|
284
|
-
###
|
318
|
+
### FmRest::Layout.request_auth_token
|
285
319
|
|
286
320
|
Requests a Data API session token using the connection settings in
|
287
321
|
`fmrest_config` and returns it if successful, otherwise returns `false`.
|
@@ -290,16 +324,16 @@ You normally don't need to use this method as fmrest-ruby will automatically
|
|
290
324
|
request and store session tokens for you (provided that `:autologin` is
|
291
325
|
`true`).
|
292
326
|
|
293
|
-
###
|
327
|
+
### FmRest::Layout.logout
|
294
328
|
|
295
|
-
Use
|
329
|
+
Use `.logout` to log out from the database session (you may call it on any
|
296
330
|
model that uses the database session you want to log out from).
|
297
331
|
|
298
332
|
```ruby
|
299
333
|
Honeybee.logout
|
300
334
|
```
|
301
335
|
|
302
|
-
### Mapped
|
336
|
+
### Mapped FmRest::Layout.attributes
|
303
337
|
|
304
338
|
Spyke allows you to define your model's attributes using `attributes`, however
|
305
339
|
sometimes FileMaker's field names aren't very Ruby-ORM-friendly, especially
|
@@ -308,7 +342,7 @@ fmrest-ruby extends `attributes`' functionality to allow you to map
|
|
308
342
|
Ruby-friendly attribute names to FileMaker field names. E.g.:
|
309
343
|
|
310
344
|
```ruby
|
311
|
-
class Honeybee < FmRest::
|
345
|
+
class Honeybee < FmRest::Layout
|
312
346
|
attributes first_name: "First Name", last_name: "Last Name"
|
313
347
|
end
|
314
348
|
```
|
@@ -327,16 +361,16 @@ bee.first_name = "Queen"
|
|
327
361
|
bee.attributes # => { "First Name": "Queen", "Last Name": "Buzz" }
|
328
362
|
```
|
329
363
|
|
330
|
-
###
|
364
|
+
### FmRest::Layout.has_portal
|
331
365
|
|
332
366
|
You can define portal associations on your model wth `has_portal`, as such:
|
333
367
|
|
334
368
|
```ruby
|
335
|
-
class Honeybee < FmRest::
|
369
|
+
class Honeybee < FmRest::Layout
|
336
370
|
has_portal :flowers
|
337
371
|
end
|
338
372
|
|
339
|
-
class Flower < FmRest::
|
373
|
+
class Flower < FmRest::Layout
|
340
374
|
attributes :color, :species
|
341
375
|
end
|
342
376
|
```
|
@@ -371,8 +405,8 @@ Guides](https://guides.rubyonrails.org/active_model_basics.html#dirty).
|
|
371
405
|
Since Spyke is API-agnostic it only provides a wide-purpose `.where` method for
|
372
406
|
passing arbitrary parameters to the REST backend. fmrest-ruby however is well
|
373
407
|
aware of its backend API, so it extends Spkye models with a bunch of useful
|
374
|
-
querying methods: `.query`, `.
|
375
|
-
etc.
|
408
|
+
querying methods: `.query`, `.match`, `.omit`, `.limit`, `.offset`, `.sort`,
|
409
|
+
`.portal`, `.script`, etc.
|
376
410
|
|
377
411
|
See the [main document on querying](docs/Querying.md) for detailed information
|
378
412
|
on the query API methods.
|
@@ -393,7 +427,7 @@ detailed information on how those work.
|
|
393
427
|
You can define container fields on your model class with `container`:
|
394
428
|
|
395
429
|
```ruby
|
396
|
-
class Honeybee < FmRest::
|
430
|
+
class Honeybee < FmRest::Layout
|
397
431
|
container :photo, field_name: "Beehive Photo ID"
|
398
432
|
end
|
399
433
|
```
|
@@ -411,7 +445,7 @@ details.
|
|
411
445
|
|
412
446
|
### Setting global field values
|
413
447
|
|
414
|
-
You can call `.set_globals` on any `FmRest::
|
448
|
+
You can call `.set_globals` on any `FmRest::Layout` model to set global
|
415
449
|
field values on the database that model is configured for.
|
416
450
|
|
417
451
|
See the [main document on setting global field values](docs/GlobalFields.md)
|
@@ -435,7 +469,7 @@ FmRest.default_connection_settings = {
|
|
435
469
|
}
|
436
470
|
|
437
471
|
# Or in your model
|
438
|
-
class LoggyBee < FmRest::
|
472
|
+
class LoggyBee < FmRest::Layout
|
439
473
|
self.fmrest_config = {
|
440
474
|
host: "…",
|
441
475
|
…
|
@@ -449,7 +483,7 @@ If you need to set up more complex logging for your models can use the
|
|
449
483
|
Faraday connection, e.g.:
|
450
484
|
|
451
485
|
```ruby
|
452
|
-
class LoggyBee < FmRest::
|
486
|
+
class LoggyBee < FmRest::Layout
|
453
487
|
faraday do |conn|
|
454
488
|
conn.response :logger, MyApp.logger, bodies: true
|
455
489
|
end
|
@@ -460,31 +494,31 @@ end
|
|
460
494
|
|
461
495
|
FM Data API reference: https://fmhelp.filemaker.com/docs/18/en/dataapi/
|
462
496
|
|
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
|
497
|
+
| FM 18 Data API feature | Supported by basic connection | Supported by FmRest::Layout |
|
498
|
+
|-------------------------------------|-------------------------------|-----------------------------|
|
499
|
+
| Log in using HTTP Basic Auth | Yes | Yes |
|
500
|
+
| Log in using OAuth | No | No |
|
501
|
+
| Log in to an external data source | No | No |
|
502
|
+
| Log in using a FileMaker ID account | No | No |
|
503
|
+
| Log out | Yes | Yes |
|
504
|
+
| Get product information | Manual* | No |
|
505
|
+
| Get database names | Manual* | No |
|
506
|
+
| Get script names | Manual* | No |
|
507
|
+
| Get layout names | Manual* | No |
|
508
|
+
| Get layout metadata | Manual* | No |
|
509
|
+
| Create a record | Manual* | Yes |
|
510
|
+
| Edit a record | Manual* | Yes |
|
511
|
+
| Duplicate a record | Manual* | No |
|
512
|
+
| Delete a record | Manual* | Yes |
|
513
|
+
| Edit portal records | Manual* | Yes |
|
514
|
+
| Get a single record | Manual* | Yes |
|
515
|
+
| Get a range of records | Manual* | Yes |
|
516
|
+
| Get container data | Manual* | Yes |
|
517
|
+
| Upload container data | Manual* | Yes |
|
518
|
+
| Perform a find request | Manual* | Yes |
|
519
|
+
| Set global field values | Manual* | Yes |
|
520
|
+
| Run a script | Manual* | Yes |
|
521
|
+
| Run a script with another request | Manual* | Yes |
|
488
522
|
|
489
523
|
\* You can manually supply the URL and JSON to a `FmRest` connection.
|
490
524
|
|
@@ -496,6 +530,7 @@ the following Ruby implementations:
|
|
496
530
|
* Ruby 2.5
|
497
531
|
* Ruby 2.6
|
498
532
|
* Ruby 2.7
|
533
|
+
* Ruby 3.0
|
499
534
|
|
500
535
|
## Gem development
|
501
536
|
|
@@ -518,6 +553,6 @@ See [LICENSE.txt](LICENSE.txt).
|
|
518
553
|
|
519
554
|
## Disclaimer
|
520
555
|
|
521
|
-
This project is not sponsored by or otherwise affiliated with
|
522
|
-
an Apple subsidiary. FileMaker is a trademark of
|
523
|
-
the U.S. and other countries.
|
556
|
+
This project is not sponsored by or otherwise affiliated with Claris
|
557
|
+
International Inc., an Apple Inc. subsidiary. FileMaker is a trademark of
|
558
|
+
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
|
#
|
@@ -79,10 +76,10 @@ module FmRest
|
|
79
76
|
def serialize_values!(params)
|
80
77
|
params.transform_values! do |value|
|
81
78
|
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)
|
79
|
+
when *FmRest::V1.datetime_classes
|
80
|
+
FmRest::V1.convert_datetime_timezone(value.to_datetime, fmrest_config.timezone).strftime(FmRest::V1::Dates::FM_DATETIME_FORMAT)
|
81
|
+
when *FmRest::V1.date_classes
|
82
|
+
value.strftime(FmRest::V1::Dates::FM_DATE_FORMAT)
|
86
83
|
else
|
87
84
|
value
|
88
85
|
end
|
@@ -90,25 +87,6 @@ module FmRest
|
|
90
87
|
|
91
88
|
params
|
92
89
|
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
90
|
end
|
113
91
|
end
|
114
92
|
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.15.2
|
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-06 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.15.2
|
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.15.2
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: spyke
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -89,7 +89,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
89
89
|
- !ruby/object:Gem::Version
|
90
90
|
version: '0'
|
91
91
|
requirements: []
|
92
|
-
rubygems_version: 3.
|
92
|
+
rubygems_version: 3.0.6
|
93
93
|
signing_key:
|
94
94
|
specification_version: 4
|
95
95
|
summary: FileMaker Data API ORM client library
|