praxis 2.0.pre.31 → 2.0.pre.32
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -1
- data/CHANGELOG.md +137 -104
- data/bin/praxis +24 -1
- data/lib/praxis/application.rb +1 -1
- data/lib/praxis/blueprint.rb +17 -9
- data/lib/praxis/controller.rb +4 -0
- data/lib/praxis/docs/open_api/operation_object.rb +9 -0
- data/lib/praxis/docs/open_api/paths_object.rb +2 -2
- data/lib/praxis/docs/open_api_generator.rb +51 -21
- data/lib/praxis/field_expander.rb +1 -1
- data/lib/praxis/mapper/selector_generator.rb +1 -1
- data/lib/praxis/request.rb +1 -1
- data/lib/praxis/tasks/console.rb +3 -0
- data/lib/praxis/version.rb +1 -1
- data/spec/praxis/application_spec.rb +11 -0
- data/spec/praxis/blueprint_spec.rb +15 -0
- data/spec/praxis/controller_spec.rb +9 -0
- data/spec/praxis/request_spec.rb +10 -0
- data/tasks/thor/model.rb +3 -1
- data/tasks/thor/scaffold.rb +35 -3
- data/tasks/thor/templates/generator/scaffold/design/endpoints/collection.rb +1 -0
- data/tasks/thor/templates/generator/scaffold/design/media_types/item.rb +1 -1
- data/tasks/thor/templates/generator/scaffold/implementation/controllers/collection.rb +11 -14
- data/tasks/thor/templates/generator/scaffold/implementation/resources/item.rb +3 -7
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 481a1aedfa10aa914a0b0bba13a1b39ca3de5d9ecfd3840520674919fc82563e
|
4
|
+
data.tar.gz: 7796b1d757c700cb9e56a9d16f1471ef5930ee0fe19b3b596d948a9a8b7f8b3c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 91b4c7f698267e0a533ccc1ba73e500ed2c9570550b83dacee4cc1ff80bd615e55b66bfd9707ba25c05b38599a98da2661505aec15e96def2d0ae1b3f94f2b6e
|
7
|
+
data.tar.gz: c21b5bed2302eba252b58fa78da841c82227994796344eacc0d2b9c20608d166e3f9e5fd3e5bb73c2fa191b909891c5d901627832de94a4a594ee085be3f26b6
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -2,143 +2,175 @@
|
|
2
2
|
|
3
3
|
## next
|
4
4
|
|
5
|
+
## 2.0.pre.32
|
6
|
+
- Spruced up the scaffolding generation, to be more configurable using a `.praxis_scaffold` file at the root, where one can specify things like
|
7
|
+
the base module for all generated classes (`base`), the path of the models directory (`models_dir`) and the version to use (`version`). These, except the models directory can also be passed and overriden by command line arguments (and they would be saved into the config file to be usable in future invocations)
|
8
|
+
- More efficient validation of Blueprint structures
|
9
|
+
- Fix pseudo bug where the field expander capped subfields as soon as it encountered a type without explicit attributes (i.e., Hash). Instead, it allows any of the subfields to percolate through.
|
10
|
+
- OpenApi generation improvements:
|
11
|
+
- Add global parameters for versioning (ApiVersionHeader and ApiVersionParam) appropriately if the API is versioned by them. Have actions point to these definitions by $ref
|
12
|
+
- Expect the 'server' definition in the APIDefinition to contain url/description and 'variables' sections which might define variables in the server url
|
13
|
+
|
5
14
|
## 2.0.pre.31
|
6
|
-
|
7
|
-
|
8
|
-
|
15
|
+
|
16
|
+
- Switch the locally generated index.html file to browse the generated OpenAPI docs to use `elements` instead of `reDoc`
|
17
|
+
- Spruce up the initial Gemfile for the generated example app
|
18
|
+
- Fix Praxis::Mapper ordering code, to not prefix top level columns with the table name, as this completely confuses ActiveRecord which switches to full-on eager loading all of the associations in the top query. i.e., passing an invalid table/column name in a `references` method will trigger that (seemingly a current bug)
|
9
19
|
|
10
20
|
## 2.0.pre.30
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
21
|
+
|
22
|
+
- A few cleanup and robustness additions:
|
23
|
+
- OpenAPI: Disable overriding a description when the schema is a ref (there are known issues with UI browsers)
|
24
|
+
- Internal: use `_pk` in batch processor invocation instead of `id` (resources will now have a `_pk` method which defaults to `id`)
|
25
|
+
- Bumped gemspec Ruby dependency to >=2.7 (but note, that this is just a little relaxed for older codebase, we're fully building for 3.x)
|
26
|
+
- Backwards incompatible changes:
|
27
|
+
- Enforces property names are symbols (before strings were allowed)
|
28
|
+
- Resource properties, using the `as:` option, are now enforced to be real association names (will not accept other resource names and unroll their dependencies)
|
29
|
+
- Deprecated the `:through` option for a property. You can just use `as:` with a long, dot-separated association path directly.
|
30
|
+
- Enhanced ordering semantics for pagination to allow for sorting of deep associated fields:
|
31
|
+
- Right now, you can sort by fields such as `books.author.name` as one of the sorting components (with `+` or `-` still available)
|
32
|
+
- Introduced better attribute grouping concepts, that help in defining subgroups of attributes of the same object, and allow lazy loading of only partial subsets so that one can have expensive computations on some of them, but they will never be invoked unless necessary. See MediaType.`group` and Resoruce.`property_group` explanations below.
|
33
|
+
- Introduced a 'group' stanza in MediaTypes, to specify a structure of attributes that exist in the main object, but that we want to neatly expose as a subset (instead of having them unrolled at the top):
|
34
|
+
- You can now use things like `group subinfo do ... end` blocks, defining which attributes to group
|
35
|
+
- Internal: Underneath, the system will create a BlueprintAttributeGroup (instead of a Struct) as a way to ensure that only the individual attributes that need to be rendered, are accessed (and not load the whole struct at once). While the behavior, to the outside, is gonna be identical to a Struct (i.e., exposes attributes as methods), this distinct object implementation is very important as it allows you to have attributes in the subgroup that are expensive to compute, and can be rest assured that they will not be accessed/computed unless they are required for rendering.
|
36
|
+
- Introduced the `property_group` stanza in resources, to indicate that a property contains a substructure of attributes, each of which must be able to be loaded only when necessary. This commonly goes hand in hand with a `group` stanza in the resource's MediaType:
|
37
|
+
- Usage of property group requires the name of the substructure (a symbol), and the associated mediatype that contains the definition of the `group` struct, under the same name of the property.
|
38
|
+
- Internally, this stanza, will define a normal property, and include as dependencies all of the sub attributes read from the MediaType's property, but appending the name (and `_`) to them to avoid collisions.
|
39
|
+
- Also, it will define a method with the property name which will return a Forwarding object, which will delegate each of the attribute methods back to the original self objects. This allows the object to avoid being 'loaded' as a whole as it happens with Struct, therefore only materializing/calling the attribute that we actually need to use, selectively.
|
40
|
+
- For example, if we have the `Book` MediaType which has a group atrribute called `subinfo` with a few attributes (like `name` and `pages`), we can use `property_group :subinfo, Book` on its domain object, so that the system will:
|
41
|
+
- define a `subinfo` property which will depend on `subinfo_name` and `subinfo_pages`
|
42
|
+
- define a `subinfo` method that will return a Forwarding object, that will forward `name` and `pages` methods to `subinfo_name` and `subinfo_pages` methods of the self resource.
|
43
|
+
- with that, we just need to define our `subinfo_name` and `subinfo_page` methods in the resource (and also define property dependencies for them if we need to)
|
33
44
|
|
34
45
|
## 2.0.pre.29
|
35
|
-
|
36
|
-
|
46
|
+
|
47
|
+
- Assorted set of fixes to generate cleaner and more compliant OpenApi documents.
|
48
|
+
- Mostly in the area of multipart generation, and requirements and nullability for OpenApi 3.0
|
49
|
+
|
37
50
|
## 2.0.pre.28
|
38
|
-
|
39
|
-
|
40
|
-
|
51
|
+
|
52
|
+
- Enhance the mapper's Resource property to allow for a couple more powerful options using the `as:` keyword:
|
53
|
+
- `as: :self` will provide a way to map any further incoming fields on top of the already existing object. This is useful when we want to expose some properties for a resource, grouped within a sub structure, but that in reality they exist directly in the resource's underlying model (i.e., to organize the information of the model in a more structured/groupable way).
|
54
|
+
- `as: 'association1.association2'` allows us to traverse more than 1 association, and continue applying the incoming fields under that. This is commonly used when we want to expose a relationship on a resource, which is really coming from more than a single association level depth.
|
55
|
+
|
41
56
|
## 2.0.pre.27
|
42
|
-
|
43
|
-
|
44
|
-
|
57
|
+
|
58
|
+
- Introduce a new `as:` option for resource's `property`, to indicate that the underlying association method it is connected to, has a different name.
|
59
|
+
- This also will create a delegation function for the property name, that instead of calling the underlying association on the record, and wrapping the result with a resource instance, it will simply call the aliased method name (which is likely gonna hit the autogenerated code for that properyty, unless we have overriden it)
|
60
|
+
- With this change, the selector generator (i.e., the thing that looks at the incoming `fields` parameters and calculates which select and includes are necessary to query all the data we need), will be able to understand this aliasing cases, and properly pass along, and continue expanding any nested fields that are under the property name (before this, and further inner fields would be not included as soon as we hit a property that didn't have that direct association underneath).
|
45
61
|
|
46
62
|
## 2.0.pre.26
|
47
|
-
|
63
|
+
|
64
|
+
- Make POST action forwarding more robust against technically malformed GET requests with no body but passing `Content-Type`. This could cause issues when using the `enable_large_params_proxy_action` DSL.
|
48
65
|
|
49
66
|
## 2.0.pre.25
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
67
|
+
|
68
|
+
- Improve surfacing of requirement attributes in Structs for OpenApi generated documentation
|
69
|
+
- Introduction of a new dsl `enable_large_params_proxy_action` for GET verb action definitions. When used, two things will happen:
|
70
|
+
- A new POST verb equivalent action will be defined:
|
71
|
+
- It will have a `payload` matching the shape of the original GET's params (with the exception of any param that was originally in the URL)
|
72
|
+
- By default, the route for this new POST request is gonna have the same URL as the original GET action, but appending `/actions/<action_name>` to it. This can be customized by passing the path with the `at:` parameter of the DSL. I.e., `enable_large_params_proxy_action at: /actions/myspecialname` will change the generated path (can use the `//...` syntax to not include the prefix defined for the endpoint). NOTE: this route needs to be compatible with any params that might be defined for the URL (i.e., `:id` and such).
|
73
|
+
- This action will be fully visible and fully documented in the API generated docs. However, it will not need to have a corresponding controller implementation since it will special-forward it to the original GET action switching the parameters for the payload.
|
74
|
+
- Specifically, upon receiving a request to the POST equivalent action, Praxis will detect it is a special action and will:
|
75
|
+
- use directly the original action (i.e., will do the before/after filters and call the controller's method)
|
76
|
+
- will load the parameters for the action from the incoming payload
|
77
|
+
- This functionality is to allow having a POST counterpart to any GET requests that require long query strings, and for which the client cannot use a payload bodies (i.e,. Browser JS clients cannot send payload on GET requests).
|
78
|
+
- Performance improvement:
|
79
|
+
- Cache praxis associations' computation for ActiveRecord (so no communication with AR or DB happens after that)
|
80
|
+
- Performance improvement: Use OJ as the (faster) default JSON renderer.
|
81
|
+
- Introduce batch computation of resource attributes: This defines an optional DSL (`batch_computed`) to enable easier calculation of expensive attributes that can be calculated much more efficiently in group:
|
82
|
+
- The new DSL takes an attribute name (Symbol), options and an implementation block that is able to get a list of resource instances (a hash of them, indexed by id) and perform the computation for all of them at once.
|
83
|
+
- Defining an attribute this way, resources can be used to be much more efficiently to calculate values that can be retrieved much more efficiently in bulk, and/or that depend on other resources of the same type to do so (i.e., things that to calculate that attribute for one single resource can be greatly amortized by doing it for many).
|
84
|
+
- The provided block to calculate the value of the attribute for a collection of resources of the same type is stored as a method inside an inner module of the resource class called BatchProcessors
|
85
|
+
- The class level method is callable through `::BatchProcessors.<property_name>(rows_by_id: xxx)`. The rows_by_id: parameter has resource 'ids' as keys, and the resource instances themselves a values
|
86
|
+
- By default an instance method of the same `<property_name>` name will also be created, with a default implementation that will call the `BatchProcessor.<property_name>` with only its instance id and instance, and will return only its result from it.
|
87
|
+
- If creating the helper instance method is not desired, one can pass `with_instance_method: false` when defining the batched_computed block. This might be necessary if we want to define the method ourselves, or in cases where the resource itself has an 'id' property that is not called 'id' (in which case the implementation would not be correct as it uses the `id` property of the resource). If that's the case, disable the creation, and add your own instance method that uses the defined BatchProcessor method passing the right parameters.
|
88
|
+
- It is also possible to query which attributes for a resource class are batch computed. This is done through .batched_attributes (which returns and array of symbol names)
|
89
|
+
- NOTE: Defining batch_computed attributes needs to be done before finalization
|
72
90
|
|
73
91
|
## 2.0.pre.24
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
92
|
+
|
93
|
+
Assorted set of fixes and cleanup:
|
94
|
+
|
95
|
+
- better forwarding signature for query methods
|
96
|
+
- Fix the way with which to decide how to wrap an association (based on Enumerable isn't right, as Hashes are Enumerable as well). Wrapping decision
|
97
|
+
is now made based on the association type, and not the shape of the resulting type.
|
98
|
+
- Built handling of some multivalue and/or fuzzy matching cases in filtering params
|
99
|
+
- unrestrict mustermann's dependent version
|
100
|
+
- Support options and even passing a full type (instead of a block) in signature definitions (TypedMethods for resources)
|
81
101
|
|
82
102
|
## 2.0.pre.22
|
83
|
-
|
84
|
-
|
103
|
+
|
104
|
+
- Small fix in OpenAPI doc generation, which would detect and report more output types, even if they are only defined within the
|
105
|
+
children of anonymous types.
|
85
106
|
|
86
107
|
## 2.0.pre.22
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
108
|
+
|
109
|
+
- Introduced Resource callbacks (an includeable concern). Callbacks allow you to define methods or blocks to be executed `before`, `after` or `around` any existing method in the resource. Class-level callbacks are defined with `self.xxxxx`. These methods will be executed within the instance of the resource (i.e., in the same context of the original) and must be defined with the same parameter signature. For around methods, only blocks can be used, and to call the original (inner) one, one needs to yield.
|
110
|
+
- Introduced QueryMethods for resources (an includeable concern). QueryMethods expose handy querying methods (`.get`, `.get!`, `.all`, `.first` and `.last` ) which will reach into the underlying ORM (i.e., right now, only ActiveModelCompat is supported) to perform the desired loading of data (and subsequent wrapping of results in resource instances).
|
111
|
+
- For ActiveRecord `.get` takes a condition hash that will translate to `.find_by`, and `.all` gets a condition hash that will translate to `.where`.
|
112
|
+
- `.get!` is a `.get` but that will raise a `Praxis::Mapper::ResourceNotFound` exception if nothing was found.
|
113
|
+
- There is an `.including(<spec>)` function that can be use to preload the underlying associations. I.e., the `<spec>` argument will translate to `.includes(<spec>)` in ActiveRecord.
|
114
|
+
- Introduced method signatures and validations for resources.
|
115
|
+
- One can define a method signature with the `signature(<name>)` stanza, passing a block defining Attributor parameters. For instance method signatures, the `<name>` is just a symbol with the name of the method. For class level methods use a string, and prepend `self.` to it (i.e., `self.create`).
|
116
|
+
- Signatures can only work for methods that either have a single argument (taken as a whole hash), or that have only keyword arguments (i.e., no mixed args and kwargs). It would be basically impossible to validate that combo against an Attributor Struct.
|
117
|
+
- The calls to typed methods will be intercepted (using an around callback), and the incoming parameters will be validated against the Attributor Struct defined in the siguature, coerced if necessary and passed onto the original method. If the incoming parameters fail validation, a `IncompatibleTypeForMethodArguments` exception will be thrown.
|
96
118
|
|
97
119
|
## 2.0.pre.21
|
98
|
-
|
120
|
+
|
121
|
+
- Fix nullable attribute in OpenApi generation
|
122
|
+
|
99
123
|
## 2.0.pre.20
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
124
|
+
|
125
|
+
- Changed the behavior of dev-mode when validate_responses. Now they return a 500 status code (instead of a 400) but with the same validation error format body.
|
126
|
+
- validate_responses is meant to catch the application returning non-compliant responses for development only. As such, a 500 is much more appropriate and clear, as the validation is done on the behavior of the server, and not on the information sent by the client (i.e., it is a server problem, not reacting the way the API is defined)
|
127
|
+
- Introduced a method to reload a Resouce (.reload), which will clear the memoized values and call record.reload as well
|
128
|
+
- Open API Generation enhancements:
|
129
|
+
- Fixed type discovery (where some types wouldn't be included in the output)
|
130
|
+
- Changed the generation to output named types into components, and use `$ref` to point to them whenever appropriate
|
131
|
+
- Report nullable attributes
|
132
|
+
|
107
133
|
## 2.0.pre.19
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
134
|
+
|
135
|
+
- Introduced a new DSL for the `FilteringParams` type that allows filters for common attributes in your Media Types:
|
136
|
+
- The new `any` DSL allows you to define which final leaf attribute to always allow, and with which operators and/or fuzzy restrictions.
|
137
|
+
- For example, you can add `any updated_at, using: ['>','<']` which would allow the type to accept filters like `updated_at>2000-01-01`, or any existing nested fields like `posts.comments.updated_at>2000-01-01`
|
138
|
+
- Note that the path of attributes passed in, will still need to exist and will be validated. Also, you still need to make sure that you have the right `filters_mapping` defined in your resources.
|
139
|
+
- Changed `filters_mapping` to allow implicitly any filter path that is a valid representation of existing columns and associations. I.e., you do not have to explicitly define long nested filters that correspond to the same underlying path of associations and columns.
|
140
|
+
|
113
141
|
## 2.0.pre.18
|
114
|
-
|
115
|
-
|
142
|
+
|
143
|
+
- Upgraded to newest Attributor, which cleans up the required: true semantics to only work on keys, and introduces null: true for nullability of values (independent from presence of keys or not)
|
144
|
+
- Fixed a selector generator bug that would occur when using deep nested resource dependencies as strings 'foo.bar.baz.bam'. In this cases only partial tracking of relationships would be built, which could cause to not fully eager load DB queries.
|
145
|
+
|
116
146
|
## 2.0.pre.17
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
147
|
+
|
148
|
+
- Changed the Parameter Filtering to use left outer joins (and extra conditions), to allow for the proper results when OR clauses are involved in certain configurations.
|
149
|
+
- Built support for allowing filtering directly on associations using `!` and `!!` operators. This allows to filter results where
|
150
|
+
there are no associated rows (`!!`) or if there are some associated rows (`!`)
|
151
|
+
- Allow implicit definition of `filters_mapping` for filter names that match top-level associations of the model (i.e., like we do for the columns)
|
152
|
+
|
121
153
|
## 2.0.pre.16
|
122
154
|
|
123
|
-
|
124
|
-
|
155
|
+
- Updated `Resource.property` signature to only accept known named arguments (`dependencies` and `though` at this time) to spare anyone else from going insane wondering why their `depednencies` aren't working.
|
156
|
+
- Fixed issue with Filtering Params, that occurred with using the ! or !! operators on String-typed fields.
|
125
157
|
|
126
158
|
## 2.0.pre.14
|
127
159
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
160
|
+
- More encoding/decoding robustness for filters.
|
161
|
+
- Specs for how to encode filters are now properly defined by:
|
162
|
+
- The "value" of the filters query string needs to be URI encoded (like any other query string value). This encoding is subject to the normal rules, and therefore "could" leave some of the URI unreserved characters (i.e., 'markers') unencoded depending on the client (Section 2.2 of https://tools.ietf.org/html/rfc2396).
|
163
|
+
- The "values" for any of the conditions in the contents of the filters, however, will need to be properly "escaped" as well (prior to URL-encoding the whole syntax string itself like described above). This means that any match value needs to ensure that it has (at least) "(",")","|","&" and "," escaped as they are reserved characters for the filter expression syntax. For example, if I want to search for a name with value "Rocket&(Pants)", I need to first compose the syntax by: "name=<escaped Rocket&(Pants)>, which is "name=Rocket%26%28Pants%29" and then, just URI encode that query string value for the filters parameter in the URL like any other. For example: "filters=name%3DRocket%2526%2528Pants%2529"
|
164
|
+
- When using a multi-match (csv-separated) list of values, you need to escape each of the values as well, leaving the 'comma' unescape, as that's part of the syntax. Then uri-encode it all for the filters query string parameter value like above.
|
165
|
+
- Now, one can properly differentiate between fuzzy query prefix/postfix, and the literal data to search for (which can be or include '\*'). Report that multi-matches (i.e., csv separated values for a single field, which translate into "IN" clauses) is not allowed if fuzzy matches are received (need to use multiple OR clauses for it).
|
134
166
|
|
135
167
|
## 2.0.pre.13
|
136
168
|
|
137
|
-
|
169
|
+
- Fix filters parser regression, which would incorrectly decode url-encoded values
|
138
170
|
|
139
171
|
## 2.0.pre.12
|
140
172
|
|
141
|
-
|
173
|
+
- Rebuilt API filters to support a much richer syntax. One can now use ANDs and ORs (with ANDs having order precedence), as well as group them with parenthesis. The same individual filter operands are supported. For example: 'email=_@gmail.com&(friends.first_name=Joe_,Patty|friends.last_name=Smith)
|
142
174
|
|
143
175
|
## 2.0.pre.11
|
144
176
|
|
@@ -152,7 +184,7 @@ there are no associated rows (`!!`) or if there are some associated rows (`!`)
|
|
152
184
|
- Simple, but pervasive breaking change: Rename `ResourceDefinition` to `EndpointDefinition` (but same functionality).
|
153
185
|
- Remove all deprecated features (and raise error describing it's not supported yet)
|
154
186
|
- Remove `Links` and `LinkBuilder`. Those seem unnecessary from a Framework point of view as they aren't clear most
|
155
|
-
applications would benefit from it. Applications can choose to add that functionality on their own if so desire.
|
187
|
+
applications would benefit from it. Applications can choose to add that functionality on their own if so desire.
|
156
188
|
- Rebuilt app generators: for new empty app, and example app.
|
157
189
|
- Updated default layout to match new naming structure and more concepts commonly necessary for normal applications.
|
158
190
|
- Completely removed the native Praxis API documentation browser in lieu of OpenAPI 3.x standards, and reDoc.
|
@@ -190,6 +222,7 @@ applications would benefit from it. Applications can choose to add that function
|
|
190
222
|
- Added support for OpenAPI 3.x document generation. Consider this in Beta state, although it is fairly close to feature complete.
|
191
223
|
|
192
224
|
## 2.0.pre.4
|
225
|
+
|
193
226
|
- Reworked the field selection DB query generation to support full tree of eager loaded dependencies
|
194
227
|
- Built support for both ActiveRecord and Sequel gems
|
195
228
|
- Selected DB fields will include/map the defined resource properties and will always include any necessary fields on both sides of the joins for the given associations.
|
data/bin/praxis
CHANGED
@@ -47,6 +47,8 @@ path_to_loader = format('%<path>s/tasks/loader.thor', path: path_to_praxis)
|
|
47
47
|
load path_to_loader
|
48
48
|
|
49
49
|
class PraxisGenerator < Thor
|
50
|
+
SCAFFOLD_CONFIG_FILE = "#{Dir.pwd}/.praxis_scaffold"
|
51
|
+
|
50
52
|
# Include a few fake thor action descriptions (for the rake tasks above) so they can show up in the same usage messages
|
51
53
|
desc 'routes [json]', 'Prints the route table of the application. Defaults to table format, but can produce json'
|
52
54
|
def routes; end
|
@@ -91,7 +93,9 @@ class PraxisGenerator < Thor
|
|
91
93
|
# Cannot use the argument below or it will apply to all commands (the action in the class has it)
|
92
94
|
# argument :collection_name, required: false
|
93
95
|
# The options, however, since they're optional are fine (But need to be duplicated from the class :( )
|
94
|
-
option :
|
96
|
+
option :base, required: false,
|
97
|
+
desc: 'Module name to enclose all generated files. Empty by default. You can pass things like MyApp, or MyApp::SubModule'
|
98
|
+
option :version, required: false,
|
95
99
|
desc: 'Version string for the API endpoint. This also dictates the directory structure (i.e., v1/endpoints/...))'
|
96
100
|
option :design, type: :boolean, default: true,
|
97
101
|
desc: 'Include the Endpoint and MediaType files for the collection'
|
@@ -120,6 +124,25 @@ class PraxisGenerator < Thor
|
|
120
124
|
opts = { orm: options[:model] }
|
121
125
|
opts[:orm] = 'activerecord' if opts[:orm] == 'model' # value is model param passed by no value
|
122
126
|
::PraxisGen::Model.new([collection_name.singularize], opts).invoke(:g)
|
127
|
+
self.class.save_scaffolding_config
|
128
|
+
end
|
129
|
+
|
130
|
+
# Read and pass the hash around in the class, so callers can read and modify it if desired
|
131
|
+
# Final contents will be saved at the end of scaffolding generation
|
132
|
+
def self.scaffold_config
|
133
|
+
return @current_config if @current_config
|
134
|
+
|
135
|
+
@current_config = File.exist?(SCAFFOLD_CONFIG_FILE) ? JSON.parse(File.read(SCAFFOLD_CONFIG_FILE), symbolize_names: true) : {}
|
136
|
+
end
|
137
|
+
|
138
|
+
def self.save_scaffolding_config
|
139
|
+
if File.exist?(SCAFFOLD_CONFIG_FILE)
|
140
|
+
contents_from_file = JSON.parse(File.read(SCAFFOLD_CONFIG_FILE), symbolize_names: true)
|
141
|
+
return if contents_from_file == @current_config
|
142
|
+
end
|
143
|
+
|
144
|
+
puts "Saving new scaffolding config into #{SCAFFOLD_CONFIG_FILE}"
|
145
|
+
File.write(SCAFFOLD_CONFIG_FILE, JSON.pretty_generate(scaffold_config))
|
123
146
|
end
|
124
147
|
|
125
148
|
# Initially, the idea was to build some quick model generator, but I think it's better to keep it
|
data/lib/praxis/application.rb
CHANGED
data/lib/praxis/blueprint.rb
CHANGED
@@ -333,23 +333,31 @@ module Praxis
|
|
333
333
|
errors = []
|
334
334
|
keys_provided = []
|
335
335
|
|
336
|
-
|
337
|
-
sub_context = self.class.generate_subcontext(context, key)
|
338
|
-
value = _get_attr(key)
|
339
|
-
keys_provided << key if @object.key?(key)
|
336
|
+
keys_provided = object.contents.keys
|
340
337
|
|
341
|
-
|
338
|
+
keys_provided.each do |key|
|
339
|
+
sub_context = self.class.generate_subcontext(context, key)
|
340
|
+
attribute = self.class.attributes[key]
|
342
341
|
|
343
|
-
|
344
|
-
|
345
|
-
if @object[key].nil?
|
346
|
-
errors.concat ["Attribute #{Attributor.humanize_context(sub_context)} is not nullable."] if !Attributor::Attribute.nullable_attribute?(attribute.options) && @object.key?(key) # It is only nullable if there's an explicite null: true (undefined defaults to false)
|
342
|
+
if object.contents[key].nil?
|
343
|
+
errors.concat ["Attribute #{Attributor.humanize_context(sub_context)} is not nullable."] if !Attributor::Attribute.nullable_attribute?(attribute.options) && object.contents.key?(key) # It is only nullable if there's an explicite null: true (undefined defaults to false)
|
347
344
|
# No need to validate the attribute further if the key wasn't passed...(or we would get nullable errors etc..cause the attribute has no
|
348
345
|
# context if its containing key was even passed (and there might not be a containing key for a top level attribute anyways))
|
349
346
|
else
|
347
|
+
value = _get_attr(key)
|
348
|
+
next if value.respond_to?(:validating) && value.validating # really, it's a thing with sub-attributes
|
349
|
+
|
350
350
|
errors.concat attribute.validate(value, sub_context)
|
351
351
|
end
|
352
352
|
end
|
353
|
+
|
354
|
+
leftover = self.class.attributes.keys - keys_provided
|
355
|
+
leftover.each do |key|
|
356
|
+
attribute = self.class.attributes[key]
|
357
|
+
|
358
|
+
errors.concat ["Attribute #{Attributor.humanize_context(sub_context)} is required."] if attribute.options[:required]
|
359
|
+
end
|
360
|
+
|
353
361
|
self.class.attribute.type.requirements.each do |requirement|
|
354
362
|
validation_errors = requirement.validate(keys_provided, context)
|
355
363
|
errors.concat(validation_errors) unless validation_errors.empty?
|
data/lib/praxis/controller.rb
CHANGED
@@ -31,6 +31,15 @@ module Praxis
|
|
31
31
|
# security: [{}]
|
32
32
|
# servers: [{}]
|
33
33
|
}
|
34
|
+
|
35
|
+
# Handle versioning header/params for the action in a special way, by linking to the existing component
|
36
|
+
# spec that will be generated globally
|
37
|
+
api_info = ApiDefinition.instance.infos[action.endpoint_definition.version]
|
38
|
+
if (version_with = api_info.version_with)
|
39
|
+
all_parameters.push('$ref' => '#/components/parameters/ApiVersionHeader') if version_with.include?(:header)
|
40
|
+
all_parameters.push('$ref' => '#/components/parameters/ApiVersionParam') if version_with.include?(:params)
|
41
|
+
end
|
42
|
+
|
34
43
|
h[:description] = action.description if action.description
|
35
44
|
h[:tags] = all_tags.uniq unless all_tags.empty?
|
36
45
|
h[:parameters] = all_parameters unless all_parameters.empty?
|
@@ -32,14 +32,14 @@ module Praxis
|
|
32
32
|
id = resource.id
|
33
33
|
# fill in the paths hash with a key for each path for each action/route
|
34
34
|
resource.actions.each do |action_name, action|
|
35
|
-
params_example =
|
35
|
+
params_example = action.params ? action.params.example(nil) : nil
|
36
36
|
url = ActionDefinition.url_description(route: action.route, params: action.params, params_example: params_example)
|
37
37
|
|
38
38
|
verb = url[:verb].downcase
|
39
39
|
templetized_path = OpenApiGenerator.templatize_url(url[:path])
|
40
40
|
path_entry = paths[templetized_path]
|
41
41
|
# Let's fill in verb stuff within the working hash
|
42
|
-
raise "VERB #{
|
42
|
+
raise "VERB #{verb} already defined for #{id}!?!?!" if path_entry[verb]
|
43
43
|
|
44
44
|
action_uid = "action-#{action_name}-#{id}"
|
45
45
|
# Add a tag matching the resource name (hoping all actions of a resource are grouped)
|
@@ -109,7 +109,15 @@ module Praxis
|
|
109
109
|
|
110
110
|
info_object = OpenApi::InfoObject.new(version: version, api_definition_info: @infos[version])
|
111
111
|
# We only support a server in Praxis ... so we'll use the base path
|
112
|
-
|
112
|
+
server_params = {}
|
113
|
+
if(server_info = @infos[version].server)
|
114
|
+
server_params[:url] = server_info[:url]
|
115
|
+
server_params[:variables] = server_info[:variables] if server_info[:variables]
|
116
|
+
else
|
117
|
+
server_params[:url] = @infos[version].base_path
|
118
|
+
end
|
119
|
+
server_params[:description] = server_info[:description] if server_info[:description]
|
120
|
+
server_object = OpenApi::ServerObject.new(**server_params)
|
113
121
|
|
114
122
|
paths_object = OpenApi::PathsObject.new(resources: resources_by_version[version])
|
115
123
|
|
@@ -151,6 +159,28 @@ module Praxis
|
|
151
159
|
schemas: component_schemas
|
152
160
|
}
|
153
161
|
|
162
|
+
# Common params/headers for versioning (actions will link to them when appropriate, by name)
|
163
|
+
if (version_with = @infos[version].version_with)
|
164
|
+
common_params = {}
|
165
|
+
if version_with.include?(:header)
|
166
|
+
common_params['ApiVersionHeader'] = {
|
167
|
+
in: 'header',
|
168
|
+
name: 'X-Api-Version',
|
169
|
+
schema: { type: 'string', enum: [version]},
|
170
|
+
required: version_with.size == 1
|
171
|
+
}
|
172
|
+
end
|
173
|
+
if version_with.include?(:params)
|
174
|
+
common_params['ApiVersionParam'] = {
|
175
|
+
in: :query,
|
176
|
+
name: 'api_version',
|
177
|
+
schema: { type: 'string', enum: [version]},
|
178
|
+
required: version_with.size == 1
|
179
|
+
}
|
180
|
+
end
|
181
|
+
full_data[:components][:parameters] = common_params
|
182
|
+
end
|
183
|
+
|
154
184
|
# REDOC specific grouping of sidebar
|
155
185
|
resource_tags = { name: 'Resources', tags: tags_for_resources.map { |t| t[:name] } }
|
156
186
|
schema_tags = { name: 'Models', tags: tags_for_mts.map { |t| t[:name] } }
|
@@ -170,26 +200,26 @@ module Praxis
|
|
170
200
|
converted_full_data = JSON.parse(json_data) # So symbols disappear
|
171
201
|
File.open("#{filename}.yml", 'w') { |f| f.write(YAML.dump(converted_full_data)) }
|
172
202
|
|
173
|
-
html =
|
174
|
-
<!doctype html>
|
175
|
-
<html lang="en">
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
</html>
|
203
|
+
html = <<~HTML
|
204
|
+
<!doctype html>
|
205
|
+
<html lang="en">
|
206
|
+
<head>
|
207
|
+
<meta charset="utf-8">
|
208
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
209
|
+
<title>Elements in HTML</title>
|
210
|
+
#{' '}
|
211
|
+
<script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
|
212
|
+
<link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css">
|
213
|
+
</head>
|
214
|
+
<body>
|
215
|
+
|
216
|
+
<elements-api
|
217
|
+
apiDescriptionUrl="http://localhost:9090/#{version_file}/openapi.json"
|
218
|
+
router="hash"
|
219
|
+
/>
|
220
|
+
|
221
|
+
</body>
|
222
|
+
</html>
|
193
223
|
HTML
|
194
224
|
html_file = File.join(doc_root_dir, version_file, 'index.html')
|
195
225
|
File.write(html_file, html)
|
@@ -68,7 +68,7 @@ module Praxis
|
|
68
68
|
end
|
69
69
|
|
70
70
|
# just include the full thing if it has no attributes
|
71
|
-
return
|
71
|
+
return fields if object.attributes.empty?
|
72
72
|
|
73
73
|
# True, expands to the default fieldset for blueprints
|
74
74
|
fields = object.default_fieldset if object < Praxis::Blueprint && fields == true
|
@@ -321,7 +321,7 @@ module Praxis
|
|
321
321
|
end
|
322
322
|
|
323
323
|
def inspect
|
324
|
-
"
|
324
|
+
"#<#{self.class} @resource=#{@resource.name.inspect} @select=#{@select.inspect} @select_star=#{@select_star.inspect} @tracking.keys=#{@tracks.keys} (recursion omitted)>"
|
325
325
|
end
|
326
326
|
end
|
327
327
|
|
data/lib/praxis/request.rb
CHANGED
@@ -184,7 +184,7 @@ module Praxis
|
|
184
184
|
# Override the inspect instance method of a request, as, by default, the kernel inspect will go nuts
|
185
185
|
# traversing the action and app_instance and therefore all associated instance variables reachable through that
|
186
186
|
def inspect
|
187
|
-
"
|
187
|
+
"#<#{self.class}##{object_id} @action=#{@action.inspect} @params=#{@params.inspect}>"
|
188
188
|
end
|
189
189
|
end
|
190
190
|
end
|
data/lib/praxis/tasks/console.rb
CHANGED
data/lib/praxis/version.rb
CHANGED
@@ -97,6 +97,17 @@ describe Praxis::Application do
|
|
97
97
|
end
|
98
98
|
end
|
99
99
|
|
100
|
+
describe '#inspect' do
|
101
|
+
let(:klass) { Class.new(Praxis::Application) }
|
102
|
+
subject { klass.instance }
|
103
|
+
|
104
|
+
it 'includes name, object ID and root' do
|
105
|
+
SomeApplication = klass # de-anonymize class name
|
106
|
+
klass.instance.instance_variable_set(:@root, '/tmp')
|
107
|
+
expect(subject.inspect).to match(%r{#<SomeApplication#[0-9]+ @root=/tmp>})
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
100
111
|
describe '#setup' do
|
101
112
|
subject { Class.new(Praxis::Application).instance }
|
102
113
|
|
@@ -166,6 +166,12 @@ describe Praxis::Blueprint do
|
|
166
166
|
it { should be_empty }
|
167
167
|
end
|
168
168
|
|
169
|
+
context 'with a valid nested blueprint' do
|
170
|
+
let(:hash) { { name: 'bob', myself: { name: 'PseudoBob'}} }
|
171
|
+
|
172
|
+
it { should be_empty }
|
173
|
+
end
|
174
|
+
|
169
175
|
context 'with invalid sub-attribute' do
|
170
176
|
let(:hash) { { name: 'bob', address: { state: 'ME' } } }
|
171
177
|
|
@@ -173,6 +179,15 @@ describe Praxis::Blueprint do
|
|
173
179
|
its(:first) { should =~ /Attribute \$.address.state/ }
|
174
180
|
end
|
175
181
|
|
182
|
+
context 'with an invalid nested blueprint' do
|
183
|
+
let(:hash) { { name: 'bob', myself: { name: 'PseudoBob', address: { state: 'ME' }}} }
|
184
|
+
|
185
|
+
it { should have(1).item }
|
186
|
+
its(:first) { should =~ /Attribute \$.myself.address.state/ }
|
187
|
+
|
188
|
+
end
|
189
|
+
|
190
|
+
|
176
191
|
context 'for objects of the wrong type' do
|
177
192
|
it 'raises an error' do
|
178
193
|
expect do
|
@@ -31,4 +31,13 @@ describe Praxis::Controller do
|
|
31
31
|
expect(subject).to eq(PeopleResource.controller)
|
32
32
|
end
|
33
33
|
end
|
34
|
+
|
35
|
+
describe '#inspect' do
|
36
|
+
it 'includes name, object ID and request' do
|
37
|
+
SomeController = subject # de-anonymize class name
|
38
|
+
expect(subject.new('eioio').inspect).to match(
|
39
|
+
/#<SomeController#[0-9]+ @request="eioio">/
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
34
43
|
end
|
data/spec/praxis/request_spec.rb
CHANGED
@@ -146,6 +146,16 @@ describe Praxis::Request do
|
|
146
146
|
end
|
147
147
|
end
|
148
148
|
|
149
|
+
context '#inspect' do
|
150
|
+
it 'includes action and params' do
|
151
|
+
request.action = 'eioio'
|
152
|
+
request.params = 'zzyzx'
|
153
|
+
expect(request.inspect).to match(
|
154
|
+
/#<Praxis::Request#[0-9]+ @action="eioio" @params="zzyzx">/
|
155
|
+
)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
149
159
|
context '#load_headers' do
|
150
160
|
it 'is done preserving the original case' do
|
151
161
|
request.load_headers(context[:headers])
|
data/tasks/thor/model.rb
CHANGED
@@ -13,6 +13,8 @@ module PraxisGen
|
|
13
13
|
argument :model_name, required: true
|
14
14
|
option :orm, required: false, default: 'activerecord', enum: %w[activerecord sequel]
|
15
15
|
def g
|
16
|
+
models_dir = 'app/models'
|
17
|
+
models_dir = PraxisGenerator.scaffold_config[:models_dir] if PraxisGenerator.scaffold_config[:models_dir]
|
16
18
|
# self.class.check_name(model_name)
|
17
19
|
template_file = \
|
18
20
|
if options[:orm] == 'activerecord'
|
@@ -21,7 +23,7 @@ module PraxisGen
|
|
21
23
|
'models/sequel.rb'
|
22
24
|
end
|
23
25
|
puts "Generating Model for #{model_name}"
|
24
|
-
template template_file, "
|
26
|
+
template template_file, "#{models_dir}/#{model_name}.rb"
|
25
27
|
nil
|
26
28
|
end
|
27
29
|
# Helper functions (which are available in the ERB contexts)
|
data/tasks/thor/scaffold.rb
CHANGED
@@ -13,7 +13,9 @@ module PraxisGen
|
|
13
13
|
|
14
14
|
desc 'g', 'Generates an API design and implementation scaffold for managing a collection of <collection_name>'
|
15
15
|
argument :collection_name, required: true
|
16
|
-
option :
|
16
|
+
option :base, required: false,
|
17
|
+
desc: 'Module name to enclose all generated files. Empty by default. You can pass things like MyApp, or MyApp::SubModule'
|
18
|
+
option :version, required: false,
|
17
19
|
desc: 'Version string for the API endpoint. This also dictates the directory structure (i.e., v1/endpoints/...))'
|
18
20
|
option :design, type: :boolean, default: true,
|
19
21
|
desc: 'Include the Endpoint and MediaType files for the collection'
|
@@ -26,6 +28,11 @@ module PraxisGen
|
|
26
28
|
option :actions, type: :string, default: 'crud', enum: %w[cr cru crud u ud d],
|
27
29
|
desc: 'Specifies the actions to generate for the API. cr=create, u=update, d=delete. Index and show actions are always generated'
|
28
30
|
def g
|
31
|
+
incorporate_config_options
|
32
|
+
# Good defaults
|
33
|
+
options[:version] = '1' unless options[:version].presence
|
34
|
+
options[:models_dir] = 'app/models' unless options[:models_dir]
|
35
|
+
|
29
36
|
self.class.check_name(collection_name)
|
30
37
|
@actions_hash = self.class.compose_actions_hash(options[:actions])
|
31
38
|
env_rb = Pathname.new(destination_root) + Pathname.new('config/environment.rb')
|
@@ -53,10 +60,31 @@ module PraxisGen
|
|
53
60
|
template 'implementation/controllers/collection.rb', "app/#{version_dir}/controllers/#{collection_name}.rb"
|
54
61
|
end
|
55
62
|
nil
|
63
|
+
save_last_config_options
|
56
64
|
end
|
57
65
|
|
58
66
|
# Helper functions (which are available in the ERB contexts)
|
59
67
|
no_commands do
|
68
|
+
def incorporate_config_options
|
69
|
+
@saved_original_options = options
|
70
|
+
self.options = options.dup
|
71
|
+
return if PraxisGenerator.scaffold_config.empty?
|
72
|
+
|
73
|
+
begin
|
74
|
+
options[:base] = PraxisGenerator.scaffold_config[:base] unless PraxisGenerator.scaffold_config[:base].presence
|
75
|
+
options[:version] = PraxisGenerator.scaffold_config[:version] unless PraxisGenerator.scaffold_config[:version].presence
|
76
|
+
options[:models_dir] = PraxisGenerator.scaffold_config[:models_dir] if PraxisGenerator.scaffold_config[:models_dir]
|
77
|
+
rescue StandardError # rubocop:disable Lint/SuppressedException
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def save_last_config_options
|
82
|
+
return if @saved_original_options.slice('base', 'version').empty?
|
83
|
+
|
84
|
+
opts_to_save = options.slice('base', 'version').transform_keys(&:to_sym).reject { |_k, v| v.nil? }
|
85
|
+
PraxisGenerator.scaffold_config.merge!(opts_to_save)
|
86
|
+
end
|
87
|
+
|
60
88
|
def plural_class
|
61
89
|
collection_name.camelize
|
62
90
|
end
|
@@ -65,16 +93,20 @@ module PraxisGen
|
|
65
93
|
collection_name.singularize.camelize
|
66
94
|
end
|
67
95
|
|
96
|
+
def base_module
|
97
|
+
options[:base]
|
98
|
+
end
|
99
|
+
|
68
100
|
def version
|
69
101
|
options[:version]
|
70
102
|
end
|
71
103
|
|
72
104
|
def version_module
|
73
|
-
"V#{version}"
|
105
|
+
base_module.presence ? "#{base_module}::V#{version}" : "V#{version}"
|
74
106
|
end
|
75
107
|
|
76
108
|
def version_dir
|
77
|
-
|
109
|
+
"v#{version}"
|
78
110
|
end
|
79
111
|
|
80
112
|
def action_enabled?(action)
|
@@ -5,7 +5,7 @@ module <%= version_module %>
|
|
5
5
|
class <%= singular_class %> < Praxis::MediaType
|
6
6
|
identifier 'application/json'
|
7
7
|
|
8
|
-
domain_model '
|
8
|
+
domain_model 'Resources::<%= singular_class %>'
|
9
9
|
description 'Structural definition of a <%= singular_class %>'
|
10
10
|
|
11
11
|
attributes do
|
@@ -11,7 +11,7 @@ module <%= version_module %>
|
|
11
11
|
# Retrieve all <%= plural_class %> with the right necessary associations
|
12
12
|
# and render them appropriately with the requested field selection
|
13
13
|
def index
|
14
|
-
objects = build_query(model_class)
|
14
|
+
objects = build_query(model_class)
|
15
15
|
display(objects)
|
16
16
|
end
|
17
17
|
<%- end -%>
|
@@ -42,14 +42,12 @@ module <%= version_module %>
|
|
42
42
|
<%- if action_enabled?(:update) -%>
|
43
43
|
# Updates some of the information of a <%= singular_class %>
|
44
44
|
def update(id:)
|
45
|
-
# A good pattern is to
|
46
|
-
# passing the incoming
|
47
|
-
|
48
|
-
|
49
|
-
payload: request.payload,
|
50
|
-
)
|
51
|
-
return Praxis::Responses::NotFound.new unless updated_resource
|
45
|
+
# A good pattern is to retrieve the resource instance by id, and then
|
46
|
+
# call the same name method on it, by passing the incoming payload (or massaging it first)
|
47
|
+
resource = Resources::<%= singular_class %>.get(id: id)
|
48
|
+
return Praxis::Responses::NotFound.new unless resource
|
52
49
|
|
50
|
+
resource.update(payload: request.payload)
|
53
51
|
Praxis::Responses::NoContent.new
|
54
52
|
end
|
55
53
|
<%- end -%>
|
@@ -57,13 +55,12 @@ module <%= version_module %>
|
|
57
55
|
<%- if action_enabled?(:delete) -%>
|
58
56
|
# Deletes an existing <%= singular_class %>
|
59
57
|
def delete(id:)
|
60
|
-
# A good pattern is to
|
61
|
-
#
|
62
|
-
|
63
|
-
|
64
|
-
)
|
65
|
-
return Praxis::Responses::NotFound.new unless deleted_resource
|
58
|
+
# A good pattern is to retrieve the resource instance by id, and then
|
59
|
+
# call the same name method on it
|
60
|
+
resource = Resources::<%= singular_class %>.get(id: id)
|
61
|
+
return Praxis::Responses::NotFound.new unless resource
|
66
62
|
|
63
|
+
resource.delete(payload: request.payload)
|
67
64
|
Praxis::Responses::NoContent.new
|
68
65
|
end
|
69
66
|
<%- end -%>
|
@@ -22,21 +22,17 @@ module <%= version_module %>
|
|
22
22
|
<%- end -%>
|
23
23
|
|
24
24
|
<%- if action_enabled?(:update) -%>
|
25
|
-
def
|
26
|
-
record = model.find_by(id: id)
|
27
|
-
return nil unless record
|
25
|
+
def update(payload:)
|
28
26
|
# Assuming the API field names directly map the the model attributes. Massage if appropriate.
|
29
27
|
record.update(**payload.to_h)
|
30
|
-
self
|
28
|
+
self
|
31
29
|
end
|
32
30
|
<%- end -%>
|
33
31
|
|
34
32
|
<%- if action_enabled?(:delete) -%>
|
35
33
|
def self.delete(id:)
|
36
|
-
record = model.find_by(id: id)
|
37
|
-
return nil unless record
|
38
34
|
record.destroy
|
39
|
-
self
|
35
|
+
self
|
40
36
|
end
|
41
37
|
<%- end -%>
|
42
38
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: praxis
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.pre.
|
4
|
+
version: 2.0.pre.32
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josep M. Blanquer
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2023-
|
12
|
+
date: 2023-05-01 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activesupport
|