44 Commits

Author SHA1 Message Date
Alessio Gravili
098fe10ade chore: deflake joins e2e tests (#11034)
Previously, data created by other tests was also leaking into unrelated tests, causing them to fail. The new reset-db-between-tests logic added by this PR fixes this. 

Additionally, this increases playwright timeouts for CI, and adds a specific timeout override for opening a drawer, as it was incredibly slow in CI
2025-02-07 02:38:38 +00:00
Sasha
2f66bdc2dc fix(ui): create-first-user crashes when users collection has join field (#10871)
Fixes https://github.com/payloadcms/payload/issues/10870
Now we hide join fields from the `/create-first-user` view since they're
not meaningful there.
2025-01-29 19:52:22 +02:00
Alessio Gravili
344b23139e ci: fix run e2e command (#10779) 2025-01-24 03:47:49 +00:00
Sasha
25a70ab455 fix: join field with the target relationship inside localized array (#10621)
Fixes https://github.com/payloadcms/payload/issues/10356
2025-01-21 02:18:26 +02:00
Sasha
ba228dd0f3 fix: allow to set maxDepth: 0 for join fields, improve join field JSDoc (#10336)
Allows to set `maxDepth: 0` for join fields and improves JSDoc about
`maxDepth`, adds tests to confirm that
https://github.com/payloadcms/payload/issues/10243 is not an issue, but
just happens because the default `maxDepth` is `1`.
2025-01-03 14:25:19 -05:00
Jacob Fletcher
7928ecaee7 fix: safely executes form state conditions, validations, and default values (#10275)
Whenever form state fails, like when field conditions, validations, or
default value functions throw errors, blocks and array rows are stuck
within an infinite loading state. Examples of this might be when
accessing properties of undefined within these functions, etc. Although
these errors are logged to the server console, the UI is be misleading,
where the user often waits for the request to resolve rather than
understanding that an underlying API error has occurred. Now, we safely
execute these functions within a `try...catch` block and handle their
failures accordingly. On the client, form state will resolve as expected
using the default return values for these functions.
2025-01-02 14:12:28 -05:00
Jacob Fletcher
270ac10fb4 test: consolidates list view e2e tests (#10263)
There were a handful of list view e2e tests written into the text and
email field test suite, making them hard to find as they were isolated
from other related tests. A few of these tests were also duplicative
across suites, making CI run them twice unnecessarily.
2024-12-30 21:09:42 +00:00
Sasha
a0d8131649 fix(db-postgres): joins to self collection (#10182)
### What?
With Postgres, before join to self like:
```ts
import type { CollectionConfig } from 'payload'

export const SelfJoins: CollectionConfig = {
  slug: 'self-joins',
  fields: [
    {
      name: 'rel',
      type: 'relationship',
      relationTo: 'self-joins',
    },
    {
      name: 'joins',
      type: 'join',
      on: 'rel',
      collection: 'self-joins',
    },
  ],
}
```
wasn't possible, even though it's a valid usage.

### How?
Now, to differentiate parent `self_joins` and children `self_joins` we
do additional alias for the nested select -
`"4d3cf2b6_1adf_46a8_b6d2_3e1c3809d737"`:
```sql
select 
  "id", 
  "rel_id", 
  "updated_at", 
  "created_at", 
  (
    select 
      coalesce(
        json_agg(
          json_build_object('id', "joins_alias".id)
        ), 
        '[]' :: json
      ) 
    from 
      (
        select 
          "created_at", 
          "rel_id", 
          "id" 
        from 
          "self_joins" "4d3cf2b6_1adf_46a8_b6d2_3e1c3809d737" 
        where 
          "4d3cf2b6_1adf_46a8_b6d2_3e1c3809d737"."rel_id" = "self_joins"."id" 
        order by 
          "4d3cf2b6_1adf_46a8_b6d2_3e1c3809d737"."created_at" desc 
        limit 
          $1
      ) "joins_alias"
  ) as "joins_alias" 
from 
  "self_joins" 
where 
  "self_joins"."id" = $2 
order by 
  "self_joins"."created_at" desc 
limit 
  $3

```

Fixes https://github.com/payloadcms/payload/issues/10144

-->
2024-12-26 20:47:49 +02:00
Sasha
b08ff88fbd fix(db-mongodb): mongodb optimizations (#10120)
There are few issues introduced in #9594 that we need to look into with
these changes.
2024-12-21 07:42:44 -05:00
Said Akhrarov
a58b9fc230 fix(ui): join table row still shows after deletion (#9783)
<!--

Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.

Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.

The following items will ensure that your PR is handled as smoothly as
possible:

- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Fixes #

-->
### What?
This PR fixes an issue where deleting an entry in a `Join` via the
`Drawer` accessed through the `DrawerLink` would not update the table
until the page was refreshed.

### Why?
For a better, more reactive, deletion experience for end-users. Ideally,
the deletion is reflected in the table right away.

### How?
By passing an `onDrawerDelete` function to the `DrawerLink` which simply
filters out the existing doc according to an id.

Fixes #9580

Before:

[Editing---Post--before-Payload.webm](https://github.com/user-attachments/assets/3dd4df78-bb63-46b1-bf5f-7643935e15ad)

After:

[Editing---Post--after-Payload.webm](https://github.com/user-attachments/assets/97bb604f-41df-4cc9-8c46-9a59a19c72b7)
2024-12-20 21:47:09 -05:00
Alessio Gravili
b3308736c4 feat: jsdocs for generated types, by using admin.description (#9917)
This makes use of admin.description to generate JSDocs for field,
collection and global generated types.


![image](https://github.com/user-attachments/assets/980d825f-49a2-426d-933a-2ff3d205ea24)


![image](https://github.com/user-attachments/assets/d0b1f288-1ea1-4d80-8c05-003d59a4e41a)

For the future, we should add a dedicated property to override these
JSDocs.

You can view the effect of this PR on our test suite generated types
here:
05f552bbbc
2024-12-19 22:22:43 -05:00
Dan Ribbens
d03658de01 feat: join field with polymorphic relationships (#9990)
### What?
The join field had a limitation imposed that prevents it from targeting
polymorphic relationship fields. With this change we can support any
relationship fields.

### Why?
Improves the functionality of join field.

### How?
Extended the database adapters and removed the config sanitization that
would throw an error when polymorphic relationships were used.

Fixes #
2024-12-19 22:34:52 +00:00
Sasha
e468292039 perf(db-mongodb): improve performance of all operations, up to 50% faster (#9594)
This PR improves speed and memory efficiency across all operations with
the Mongoose adapter.

### How?

- Removes Mongoose layer from all database calls, instead uses MongoDB
directly. (this doesn't remove building mongoose schema since it's still
needed for indexes + users in theory can use it)
- Replaces deep copying of read results using
`JSON.parse(JSON.stringify(data))` with the `transform` `operation:
'read'` function which converts Date's, ObjectID's in relationships /
joins to strings. As before, it also handles transformations for write
operations.
- Faster `hasNearConstraint` for potentially large `where`'s
- `traverseFields` now can accept `flattenedFields` which we use in
`transform`. Less recursive calls with tabs/rows/collapsible

Additional fixes
- Uses current transaction for querying nested relationships properties
in `buildQuery`, previously it wasn't used which could've led to wrong
results
- Allows to clear not required point fields with passing `null` from the
Local API. Previously it didn't work in both, MongoDB and Postgres

Benchmarks using this file
https://github.com/payloadcms/payload/blob/chore/db-benchmark/test/_community/int.spec.ts

### Small Dataset Performance

| Metric | Before Optimization | After Optimization | Improvement (%) |

|---------------------------|---------------------|--------------------|-----------------|
| Average FULL (ms) | 1170 | 844 | 27.86% |
| `payload.db.create` (ms) | 1413 | 691 | 51.12% |
| `payload.db.find` (ms) | 2856 | 2204 | 22.83% |
| `payload.db.deleteMany` (ms) | 15206 | 8439 | 44.53% |
| `payload.db.updateOne` (ms) | 21444 | 12162 | 43.30% |
| `payload.db.findOne` (ms) | 159 | 112 | 29.56% |
| `payload.db.deleteOne` (ms) | 3729 | 2578 | 30.89% |
| DB small FULL (ms) | 64473 | 46451 | 27.93% |

---

### Medium Dataset Performance

| Metric | Before Optimization | After Optimization | Improvement (%) |

|---------------------------|---------------------|--------------------|-----------------|
| Average FULL (ms) | 9407 | 6210 | 33.99% |
| `payload.db.create` (ms) | 10270 | 4321 | 57.93% |
| `payload.db.find` (ms) | 20814 | 16036 | 22.93% |
| `payload.db.deleteMany` (ms) | 126351 | 61789 | 51.11% |
| `payload.db.updateOne` (ms) | 201782 | 99943 | 50.49% |
| `payload.db.findOne` (ms) | 1081 | 817 | 24.43% |
| `payload.db.deleteOne` (ms) | 28534 | 23363 | 18.12% |
| DB medium FULL (ms) | 519518 | 342194 | 34.13% |

---

### Large Dataset Performance

| Metric | Before Optimization | After Optimization | Improvement (%) |

|---------------------------|---------------------|--------------------|-----------------|
| Average FULL (ms) | 26575 | 17509 | 34.14% |
| `payload.db.create` (ms) | 29085 | 12196 | 58.08% |
| `payload.db.find` (ms) | 58497 | 43838 | 25.04% |
| `payload.db.deleteMany` (ms) | 372195 | 173218 | 53.47% |
| `payload.db.updateOne` (ms) | 544089 | 288350 | 47.00% |
| `payload.db.findOne` (ms) | 3058 | 2197 | 28.14% |
| `payload.db.deleteOne` (ms) | 82444 | 64730 | 21.49% |
| DB large FULL (ms) | 1461097 | 969714 | 33.62% |
2024-12-19 13:20:39 -05:00
Sasha
1d46b6d738 fix(ui): join field "add new" calculate initial drawer data with relationship inside blocks (#10057)
We merged https://github.com/payloadcms/payload/pull/9773 that adds
support for join field with relationships inside arrays, it just happens
that now it's also true for relationships inside blocks, added a test
case with blocks.
This just corrects initial data drawer calculation with the "add new"
button if we encounter a blocks field in join's `on`.
2024-12-19 08:45:22 +02:00
Dan Ribbens
b0b2fc6c47 feat: join field support relationships inside arrays (#9773)
### What?

Allow the join field to have a configuration `on` relationships inside
of an array, ie `on: 'myArray.myRelationship'`.

### Why?

This is a more powerful and expressive way to use the join field and not
be limited by usage of array data. For example, if you have a roles
array for multinant sites, you could add a join field on the sites to
show who the admins are.

### How?

This fixes the traverseFields function to allow the configuration to
pass sanitization. In addition, the function for querying the drizzle
tables needed to be ehanced.

Additional changes from https://github.com/payloadcms/payload/pull/9995:

- Significantly improves traverseFields and the 'join' case with a raw
query injection pattern, right now it's internal but we could expose it
at some point, for example for querying vectors.
- Fixes potential issues with not passed locale to traverseFields (it
was undefined always)
- Adds an empty array fallback for joins with localized relationships

Fixes #
https://github.com/payloadcms/payload/discussions/9643

---------

Co-authored-by: Because789 <thomas@because789.ch>
Co-authored-by: Sasha <64744993+r1tsuu@users.noreply.github.com>
2024-12-17 13:14:43 -05:00
Dan Ribbens
2ec4d0c2ef feat: join field admin.defaultColumns (#9982)
Add the ability to specify which columns should appear in the
relationship table of a join fields

The new property is in the Join field `admin.defaultColumns` and can be
set to an array of strings containing the field names in the desired
order.
2024-12-15 03:31:31 +00:00
Dan Ribbens
f5516b96da fix: edit join field not rendering (#9971)
In PR #9930 we added `overrideAccess: false` to the find operation and
failed to pass the user. This caused
https://github.com/payloadcms/payload/issues/9974 where any access
control causes the edit view to error.

The fix was to pass the user through.

This change also adds Join Field e2e tests to the CI pipeline which was
previously missing and would have caught the error.
2024-12-14 13:19:53 +00:00
Sasha
c298cbc90d chore: deflake postgres and sqlite integration joins test (#9939)
`relationTo` was specified incorrectly which led to 
```
  ● Joins Field › rEST API should not populate individual join by providing schemaPath=false

    error: insert or update on table "collection_restricted" violates foreign key constraint "collection_restricted_category_id_restricted_categories_id_fk"

      18 |       .returning()
      19 |   } else {
    > 20 |     result = await (db as TransactionPg).insert(table).values(values).returning()
```
2024-12-12 21:49:32 +02:00
Dan Ribbens
5af71fb8d0 fix: join collection read access (#9930)
Respect read access control through the join field collections for GraphQL and admin UI

fixes #9922 and #9865
2024-12-12 12:01:03 -05:00
Dan Ribbens
a11243e288 fix(ui): join field ignoring defaultSort and defaultLimit (#9766)
The join field was not respecting the defaultSort or defaultLimit of the
field configuration.

### Why?

This was never implemented.

### How?

This fix applies these correct limit and sort properties to the query,
first based on the field config and as a fallback, the collection
configuration.
2024-12-05 21:23:19 +00:00
Dan Ribbens
67179a7fb8 fix: join field description, custom components and loading state (#9703)
- [fix: join field shows loading when creating a
document](9f7a2e7936)
- [fix: join field
descriptions](90e8cdb464)
- [feat(ui): adds before & after inputs to join
field](19d43329ad)

---------

Co-authored-by: Patrik <patrik@payloadcms.com>
2024-12-03 15:58:42 -05:00
Jacob Fletcher
5d18a5288e fix: overrides entity visibility within drawers (#9546)
When using the `admin.hidden: true` property on a collection, it
rightfully removes all navigation and routing for that particular
collection. However, this also affects the expected behavior of hidden
entities when they are rendered within a drawer, such as the document
drawer or list drawer. For example, when creating a new _admin.hidden_
document through the relationship or join field, the drawer should still
render the view, despite the underlying route for that view being
disabled. This change was a result of the introduction of on-demand
server components in #8364, where we now make a server roundtrip to
render the view in its entirety, which include the logic that redirects
these hidden entities.

Now, we pass a new `overrideEntityVisibility` argument through the
server function that, when true, skips this step. This way documents can
continue to respect `admin.hidden` while also having the ability to
override on a case-by-case basis throughout the UI.
2024-11-26 18:22:04 -05:00
Jacob Fletcher
0757e06e71 feat: deprecates react-animate-height in favor of native css (#9456)
Deprecates `react-animate-height` in favor of native CSS, specifically
the `interpolate-size: allow-keywords;` property which can be used to
animate to `height: auto`—the primary reason this package exists. This
is one less dependency in our `node_modules`. Tried to replicate the
current DOM structure, class names, and API of `react-animate-height`
for best compatibility.

Note that this CSS property is experimental BUT this PR includes a patch
for browsers without native support. Once full support is reached, the
patch can be safely removed.
2024-11-25 17:48:16 -05:00
Sasha
d499de1e0f fix(db-postgres): joins with versions and hasMany relationship (#9370)
Fixes errors when having joins with versions +drafts on `hasMany: true`
relationships.
Removes `joinQuery` overhead if we don't need it for the current
operation. Right now, in all adapters we support joins only for `find`,
`findOne`, and `queryDrafts`.


Fixes https://github.com/payloadcms/payload/issues/9369
2024-11-21 10:42:05 -05:00
Jacob Fletcher
07a9125f9c fix(next): properly sets doc id types when using postgres (#9381)
Fixes #9351. When using Postgres, doc ids were being treated as a string
as opposed to a number within the admin panel. This led to issues for
anything relying on the `docID` from context, such as the join field not
properly populating initial data when creating new documents, etc.
2024-11-20 15:02:08 -05:00
Sasha
ef2475d804 fix(ui): avoid calling getTableState from join field on create (#9256)
### What?
Fixes the issue when visiting the create view with the Join Field and
using postgres adapter
```
invalid input syntax for type integer: "NaN"
```
This happens because we don't have an ID yet and we send to the
database:
`WHERE id = NaN`

### How?
Avoids calling `getTableState` inside of `RelationshipTable` if there's
no ID yet, as it will always lead to the same empty result. While we
_could_ avoid error directly in the database adapter, I don't think we
should do that render request

Fixes https://github.com/payloadcms/payload/issues/9193
2024-11-17 11:32:50 -05:00
Sasha
f67761fe22 fix(db-mongodb): totalDocs with joins (#9056)
### What?
Fixes issue with incorrect `totalDocs` value when an aggregation is used
for `find`.
Previously, `limit: 5` for example always returned `totalDocs: 5`.

### Why?
`totalDocs` must be returned correctly.

### How?
Removes `$limit` from the pipeline, as `Model.aggregatePaginate` handles
it by itself.
2024-11-08 14:04:24 -05:00
Sasha
213b7c6fb6 feat: generate types for joins (#9054)
### What?
Generates types for `joins` property.
Example from our `joins` test, keys are type-safe:
<img width="708" alt="image"
src="https://github.com/user-attachments/assets/f1fbbb9d-7c39-49a2-8aa2-a4793ae4ad7e">

Output in `payload-types.ts`:
```ts
 collectionsJoins: {
    categories: {
      relatedPosts: 'posts';
      hasManyPosts: 'posts';
      hasManyPostsLocalized: 'posts';
      'group.relatedPosts': 'posts';
      'group.camelCasePosts': 'posts';
      filtered: 'posts';
      singulars: 'singular';
    };
  };
```
Additionally, we include type information about on which collection the
join is, it will help when we have types generation for `where` and
`sort`.

### Why?
It provides a better DX as you don't need to memoize your keys.

### How?
Modifies `configToJSONSchema` to generate the json schema for
`collectionsJoins`, uses that type within `JoinQuery`
2024-11-06 22:43:07 +02:00
Dan Ribbens
93a55d1075 feat: add join field config where property (#8973)
### What?

Makes it possible to filter join documents using a `where` added
directly in the config.


### Why?

It makes the join field more powerful for adding contextual meaning to
the documents being returned. For example, maybe you have a
`requiresAction` field that you set and you can have a join that
automatically filters the documents to those that need attention.

### How?

In the database adapter, we merge the requested `where` to the `where`
defined on the field.
On the frontend the results are filtered using the `filterOptions`
property in the component.

Fixes
https://github.com/payloadcms/payload/discussions/8936
https://github.com/payloadcms/payload/discussions/8937

---------

Co-authored-by: Sasha <64744993+r1tsuu@users.noreply.github.com>
2024-11-06 10:06:25 -05:00
Sasha
d64946c2e2 fix(db-mongodb): ensure relationships are stored in ObjectID (#8932)
### What?
Since the join field, we do store relationship fields values in
`ObjectID`. This wasn't true if the field is nested to an array /
blocks.

### Why?
All relationship fields values should be stored in `ObjectID`.

### How?
Fixes arrays / blocks handling in the `traverseFields.ts` function.
Before it didn't run for them.
2024-10-30 13:42:07 -04:00
Sasha
f4041ce6e2 fix(db-mongodb): joins with singular collection name (#8933)
### What?
Properly specifies `$lookup.from` when the collection name is singular.

### Why?
MongoDB can pluralize the collection name and so can be different for
singular ones.

### How?
Uses the collection name from the driver directly
`adapter.collections[slug].collection.name` instead of just `slug`.
2024-10-30 12:06:03 -04:00
Sasha
dae832c288 feat: select fields (#8550)
Adds `select` which is used to specify the field projection for local
and rest API calls. This is available as an optimization to reduce the
payload's of requests and make the database queries more efficient.

Includes:
- [x] generate types for the `select` property
- [x] infer the return type by `select` with 2 modes - include (`field:
true`) and exclude (`field: false`)
- [x] lots of integration tests, including deep fields / localization
etc
- [x] implement the property in db adapters
- [x] implement the property in the local api for most operations
- [x] implement the property in the rest api 
- [x] docs

---------

Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2024-10-29 21:47:18 +00:00
Sasha
7a7a2f3918 fix(db-mongodb): query 'in' with a comma-delimited value (#8910)
### What?
Fixes the issue with `in` querying when the collection has a join field.

### Why?
When using `.aggregate`, MongoDB doesn't cast a comma delimited value
for the `$in` operator to an array automatically as it's not handled by
Mongoose.

### How?
Sanitizes the incoming value to an array if it should.

Fixes https://github.com/payloadcms/payload/issues/8901
2024-10-28 19:41:28 -04:00
Dan Ribbens
f0edbb79f9 feat: join field defaultLimit and defaultSort (#8908)
### What?

Allow specifying the defaultSort and defaultLimit to use for populating
a join field

### Why?

It is much easier to set defaults rather than be forced to always call
the join query using the query pattern ("?joins[categories][limit]=0").

### How?

See docs and type changes
2024-10-28 17:52:37 -04:00
Sasha
8af00f2deb fix: join field works on collections with versions enabled (#8715)
- Fixes errors with drizzle when building the schema
https://github.com/payloadcms/payload/issues/8680
- Adds `joins` to `db.queryDrafts` to have them when doing `.find` with
`draft: true`
2024-10-22 11:05:55 -04:00
Jacob Fletcher
35a5199c87 fix(next): returns proper document id type from init page result (#8700) 2024-10-14 19:39:30 -04:00
Sasha
f6acfdb1f5 fix(drizzle): hasMany joins - localized, limit and schema paths (#8633)
Fixes https://github.com/payloadcms/payload/issues/8630

- Fixes `hasMany: true` and `localized: true` on the foreign field
- Adds `limit` to the subquery instead of hardcoded `11`.
- Adds the schema path `field.on` to the subquery, without this having 2
or more relationship fields to the same collection breaks joins
- Properly checks if the field is `hasMany`
2024-10-10 12:58:30 -04:00
Dan Ribbens
1bf580fac3 feat: join field works with hasMany relationships (#8493)
Join field works on relationships and uploads having `hasMany: true`

---------

Co-authored-by: Sasha <64744993+r1tsuu@users.noreply.github.com>
2024-10-08 11:40:34 -04:00
Jacob Fletcher
2ba40f3335 chore: removes duplicative join field test (#8558)
There are two of the exact same e2e tests for the join field, which
throws an error when running these tests locally because they have
identical names.
2024-10-05 09:13:43 -04:00
Dan Ribbens
27b1629927 fix(db-postgres, db-sqlite): joins relation name (#8491)
A join field on a relationship with a camelCase name would cause an
error from drizzle due to the relation name not matching.
2024-09-30 16:47:21 -04:00
Dan Ribbens
3f375cc6ee feat: join field on upload fields (#8379)
This PR makes it possible to use the new `join` field in connection with
an `upload` field. Previously `join` was reserved only for
relationships.
2024-09-30 13:12:30 -04:00
Dan Ribbens
dc69e2c0f6 fix(db-mongodb): db.find default limit to 0 (#8376)
Fixes an error anytime a `db.find` is called on documents with joins
without a set `limit` by defaulting the limit to 0.
2024-09-23 16:31:50 -04:00
Elliot DeNolf
040c2a2fbb chore(eslint): FlatConfig type deprecated, set to Config 2024-09-20 22:46:40 -04:00
Dan Ribbens
6ef2bdea15 feat!: join field (#7518)
## Description

- Adds a new "join" field type to Payload and is supported by all database adapters
- The UI uses a table view for the new field
- `db-mongodb` changes relationships to be stored as ObjectIDs instead of strings (for now querying works using both types internally to the DB so no data migration should be necessary unless you're querying directly, see breaking changes for details
- Adds a reusable traverseFields utility to Payload to make it easier to work with nested fields, used internally and for plugin maintainers

```ts
export const Categories: CollectionConfig = {
    slug: 'categories',
    fields: [
        {
            name: 'relatedPosts',
            type: 'join',
            collection: 'posts',
            on: 'category',
        }
    ]
}
```

BREAKING CHANGES:
All mongodb relationship and upload values will be stored as MongoDB ObjectIDs instead of strings going forward. If you have existing data and you are querying data directly, outside of Payload's APIs, you get different results. For example, a `contains` query will no longer works given a partial ID of a relationship since the ObjectID requires the whole identifier to work. 

---------

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
Co-authored-by: James <james@trbl.design>
2024-09-20 11:10:16 -04:00