Compare commits

...
Author SHA1 Message Date
ArnabChatterjee20k 6da8c1cb12 updated 2026-05-08 15:21:52 +05:30
ArnabChatterjee20k d303d6f807 Refactor path parameter detection in OpenAPI3 and Swagger2 by utilizing array flipping for improved performance and clarity in matching aliases. 2026-05-08 14:39:26 +05:30
ArnabChatterjee20k 07973dee2d Refactor URL parameter replacement logic in OpenAPI3 and Swagger2 to ensure accurate matching of path parameters by checking for trailing characters. 2026-05-08 14:19:51 +05:30
ArnabChatterjee20k 5cfaa0807d Refactor URL parameter matching in OpenAPI3 and Swagger2 to improve path parameter detection by checking for trailing characters. 2026-05-08 14:08:27 +05:30
ArnabChatterjee20k e181954dd1 removed regex 2026-05-08 14:01:04 +05:30
ArnabChatterjee20k 4b05a6cf8f Refactor URL parameter matching in OpenAPI3 and Swagger2 to use preg_match for improved accuracy with path aliases. 2026-05-08 13:52:25 +05:30
ArnabChatterjee20k 29bbc7299a Enhance URL parameter handling in OpenAPI3 and Swagger2 formats to support aliases for path parameters. 2026-05-08 13:45:46 +05:30
ArnabChatterjee20k 21ba13a076 updated 2026-05-07 18:40:26 +05:30
ArnabChatterjee20k 399f75e8e7 updated 2026-05-07 16:35:56 +05:30
ArnabChatterjee20k 7917506b58 updated oauth 2026-05-07 16:30:23 +05:30
ArnabChatterjee20k 706459e314 updated 2026-05-07 16:01:52 +05:30
ArnabChatterjee20k 7e9d6f33c8 updated methods 2026-05-07 15:56:44 +05:30
Jake BarnbyandGitHub 3b2a01f470 Merge pull request #11910 from appwrite/feat/skip-duplicates
feat(migrations): add onDuplicate option to CSV/JSON/Appwrite imports
2026-05-07 19:21:47 +12:00
premtsd-codeandGitHub e621701cbc Merge branch '1.9.x' into feat/skip-duplicates 2026-05-07 12:00:15 +05:30
Luke B. SilverandGitHub c89bf3084a Merge pull request #12229 from appwrite/chore/builds
Chore/builds
2026-05-06 22:47:57 +01:00
loks0n 9387c480f5 Add Span attributes to build worker for trace observability 2026-05-06 19:56:17 +01:00
premtsd-codeandGitHub 2e9841c9c2 Merge branch '1.9.x' into feat/skip-duplicates 2026-05-06 23:35:25 +05:30
loks0n 8201fea9ef Differentiate executor timeouts for builds, sync, and async executions
Adds Executor\Exception\Timeout (with timeoutSeconds) and translates it at
each call site into BUILD_TIMEOUT, FUNCTION_SYNCHRONOUS_TIMEOUT, or
FUNCTION_ASYNCHRONOUS_TIMEOUT instead of always using the misleading sync
function error. Build timeouts now append to streamed buildLogs rather
than replacing them, and the build worker reports its timeout via Span.
2026-05-06 15:54:11 +01:00
Matej BačoandGitHub bf52b1ba31 Merge pull request #12228 from appwrite/fix-refresh-session
Fix: Refreshing non-oauth sessions
2026-05-06 16:38:01 +02:00
Matej Bačo e834a95213 PR review improvements 2026-05-06 16:21:50 +02:00
Matej Bačo a9dd957a7a Fix test 2026-05-06 15:57:25 +02:00
Matej Bačo d2b551cd12 Fix refreshing nonoauth sessions 2026-05-06 15:50:18 +02:00
Matej BačoandGitHub bc0501aaf2 Merge pull request #12224 from appwrite/fix-oauth-oidc-params
Fix: OAuth OIDC param consistency
2026-05-06 12:25:45 +02:00
Matej Bačo a65c6136f4 Improve code quality 2026-05-06 12:08:02 +02:00
Matej Bačo 78133a3c25 Merge branch '1.9.x' into fix-oauth-oidc-params 2026-05-06 11:46:09 +02:00
Matej Bačo 140cbb633d Fix tests 2026-05-06 11:45:35 +02:00
Matej Bačo 389146c625 oidc backwards compatibiltiy test 2026-05-06 11:32:00 +02:00
Matej BačoandGitHub ec927104fe Merge pull request #12221 from appwrite/fix-variables-api-feature-parity
Feat: Variables API feature parity
2026-05-06 11:27:32 +02:00
Matej Bačo dcef7ef559 OIDC param name improvement 2026-05-06 11:24:53 +02:00
Matej Bačo ff518a055f Merge branch '1.9.x' into fix-oauth-oidc-params 2026-05-06 11:17:42 +02:00
Matej Bačo 00ee9c6b0e Fix benchmark 2026-05-06 11:09:11 +02:00
Prem Palanisamy f9c5f41a1a Merge remote-tracking branch 'origin/1.9.x' into feat/skip-duplicates
# Conflicts:
#	composer.lock
2026-05-06 09:53:13 +01:00
Matej Bačo d27b578888 Fix more tests 2026-05-06 10:34:37 +02:00
Matej Bačo 1ccf058c14 Fix failing tests 2026-05-06 10:19:30 +02:00
Jake BarnbyandGitHub 1273bcd33a Merge pull request #11673 from appwrite/big-int
Big int
2026-05-06 20:17:45 +12:00
Matej BačoandGitHub add2b7b64a Merge pull request #12208 from appwrite/feat-public-proxy-api
Feat: Public Proxy API
2026-05-06 10:15:57 +02:00
ArnabChatterjee20k a0d77ba1b2 Merge remote-tracking branch 'origin/1.9.x' into big-int 2026-05-06 13:22:37 +05:30
Matej BačoandGitHub 63ac3bedca Merge pull request #12212 from appwrite/fix-param-name-consistency
Fix endpoint consistency (oauth get)
2026-05-06 09:42:22 +02:00
Matej Bačo a286b78a0b Fix function tests 2026-05-06 09:41:00 +02:00
Prem Palanisamy e63f9fd6a5 tests: send non-empty parent row data in TwoWayRecreate test
server-ce 1.9.x's tablesdb POST /rows tightened input validation: the
modular Documents/Create.php rejects `data => []` with a 400
"missing data" because TablesDB's Rows/Create.php inherits the strict
default of getSupportForEmptyDocument() = false (only DocumentsDB
overrides it to true). The test was relying on the older permissive
behavior to seed an empty parent row before the relationship cascade
links it.

Add a non-relationship `label` string column on the parents table and
populate it with `data => ['label' => 'p1']` so the POST passes the
empty-data guard. The test's actual assertion target — partner-side
pair-key dedup on DropAndRecreate — is unchanged.

Cascade fixes: testAppwriteMigrationOverwriteAttributeRecreate and
testAppwriteMigrationOverwriteSameSpecRecreate were failing in the
retry pass because TwoWayRecreate's bail at L1616 left source/dest
state uncleaned. Once TwoWayRecreate completes, those tests see a
clean project again.

Caught in CI run 25419479164 / job 74562934987 on the MongoDB
(dedicated) Migrations matrix.
2026-05-06 08:40:17 +01:00
ArnabChatterjee20kandCopilot fc918d8b3c feat: add support for unsigned integer and bigint columns in database tests
Co-authored-by: Copilot <copilot@github.com>
2026-05-06 11:48:47 +05:30
ArnabChatterjee20k 93ce542d31 updated composer 2026-05-06 11:37:05 +05:30
Prem Palanisamy e6a9c68534 Merge remote-tracking branch 'origin/1.9.x' into feat/skip-duplicates
# Conflicts:
#	composer.lock
2026-05-06 07:06:59 +01:00
ArnabChatterjee20kandCopilot 03f7b62ff1 refactor: update database version constraints and simplify bigint handling in collection creation
Co-authored-by: Copilot <copilot@github.com>
2026-05-06 11:24:22 +05:30
Damodar LohaniandGitHub ea4a31c6c6 Merge pull request #12222 from appwrite/CLO-2333-additional-indexes-1.9x
feat(indexes): add 4 missing indexes (supersedes #9629)
2026-05-06 10:50:44 +05:45
Damodar Lohani c9ad685e11 fix(indexes): use empty lengths for mixed-type composite to match existing pattern
greptile flagged missing lengths but the codebase has two conventions:
- all-string composite: [LENGTH_KEY, LENGTH_KEY] (_key_unique, _key_provider_providerUid)
- mixed/non-string composite: [] (e.g. ('enabled', 'type'), ('region', 'accessedAt'),
  ('targetInternalId', 'topicInternalId'), ('period', 'time'), ('metric', 'period', 'time'))

(teamInternalId, confirm) is string+boolean; closest match is
('enabled', 'type') which uses lengths => [] and orders => [].
Aligning to that established pattern instead of inventing
[LENGTH_KEY, 0].
2026-05-06 04:48:53 +00:00
Damodar Lohani 83d56a2f36 fix(indexes): set explicit lengths + orders on new indexes (greptile P1)
All four new indexes left lengths/orders as empty arrays; greptile
flagged the inconsistency vs every existing string-attribute index in
the file (e.g. _key_team uses [LENGTH_KEY], _key_unique uses
[LENGTH_KEY, LENGTH_KEY]).

- memberships._key_team_confirm: [LENGTH_KEY, 0] for (string, boolean)
  + [ORDER_ASC, ORDER_ASC]
- projects._key_teamInternalId: [LENGTH_KEY] + [ORDER_ASC]
- platforms._key_project_id: [LENGTH_KEY] + [ORDER_ASC]
- webhooks._key_project_id: [LENGTH_KEY] + [ORDER_ASC]
2026-05-06 04:44:58 +00:00
Damodar Lohani 94c968e941 feat(indexes): add 4 missing indexes (CLO-2333)
- memberships: _key_team_confirm on (teamInternalId, confirm) for team-membership confirm-state queries
- projects: _key_teamInternalId on teamInternalId for team-scoped project lookups
- platforms: _key_project_id on projectId for user-facing-id lookups
- webhooks: _key_project_id on projectId for user-facing-id lookups

Re-applies the indexes from the stale PR #9629 (1.7.x base, conflicting)
onto a fresh 1.9.x branch. None of these are in 1.9.x today; existing
similar indexes target projectInternalId / teamId rather than the
user-facing projectId / teamInternalId queries this addresses.
2026-05-06 04:35:29 +00:00
Matej BačoandGitHub 8918cc75c3 Merge pull request #12213 from appwrite/feat-list-endpoint-queries-oauth
Feat: queries & total params for list oauth providers
2026-05-05 17:16:09 +02:00
Matej Bačo f90f618bff Fix failing tests 2026-05-05 17:14:05 +02:00
Matej Bačo b382a9c3e5 Fix mistakes after AI 2026-05-05 16:53:24 +02:00
Matej Bačo 3fd79e3045 Merge branch '1.9.x' into fix-param-name-consistency 2026-05-05 16:52:10 +02:00
Matej Bačo 3e58500e13 Upgrade libs 2026-05-05 16:51:57 +02:00
Matej Bačo 1975ab2513 Fix linter 2026-05-05 16:45:15 +02:00
Matej Bačo 15f21daa2b Add tests 2026-05-05 16:45:09 +02:00
Matej Bačo 48fbb59113 pr review fixes 2026-05-05 16:44:47 +02:00
Matej Bačo 9b120e9db7 Remove unwanted changes 2026-05-05 16:29:09 +02:00
Matej Bačo 305e8e1ec7 Add request filter 2026-05-05 16:24:41 +02:00
Matej Bačo 93515fcc1d Finalize variables rework 2026-05-05 16:17:47 +02:00
Matej Bačo fd1a4542e7 Fix variables API feature parity 2026-05-05 16:09:17 +02:00
ArnabChatterjee20k 7ccf2b1a1e updated 2026-05-05 19:28:28 +05:30
ArnabChatterjee20k 57dd7cf952 updated 2026-05-05 19:22:00 +05:30
ArnabChatterjee20k 90e681a906 updated 2026-05-05 19:20:47 +05:30
ArnabChatterjee20k bca876acaf Merge remote-tracking branch 'origin/1.9.x' into big-int 2026-05-05 18:55:11 +05:30
Matej BačoandGitHub 2fc044a7c9 Merge pull request #12218 from appwrite/fix-double-policies-scopes
Chore: Rename policies scope
2026-05-05 13:56:03 +02:00
Matej Bačo fef4cbf3b0 Fix missing new scope 2026-05-05 13:37:51 +02:00
Matej Bačo 32d30dfd9d Fix copy 2026-05-05 13:06:57 +02:00
Matej Bačo b0220292a7 Rename policies to prevent double scope usease 2026-05-05 12:58:06 +02:00
Prem Palanisamy 9ab546d743 composer: pin utopia-php/migration to 1.9.7 (released) 2026-05-05 09:11:18 +01:00
Chirag AggarwalandGitHub 67ab29ffeb Merge pull request #12211 from appwrite/feat/spec-unified-auth-headers 2026-05-05 12:53:26 +05:30
Jake BarnbyandGitHub 1a41a8ea66 Merge pull request #11632 from appwrite/backup-migration-multitype
Add backup policy migration support
2026-05-05 19:20:55 +12:00
Chirag AggarwalandGitHub 538fa94c79 Merge pull request #12216 from appwrite/fix/graphql-php-audit 2026-05-05 11:02:31 +05:30
Chirag Aggarwal 1a410c43d7 fix: update graphql php dependency 2026-05-05 10:54:01 +05:30
Matej Bačo 4d5bb30917 tests for oauth list endpoint ueries and total 2026-05-04 16:05:56 +02:00
Matej Bačo c35d5e348b Support queries in oauth list endpoint 2026-05-04 16:05:46 +02:00
Matej Bačo d2922e7d5d Fix failing tests 2026-05-04 15:49:05 +02:00
Matej Bačo b1c3fc0908 Update tests 2026-05-04 15:40:32 +02:00
Matej Bačo d18f64d526 Fix endpoint consistency, apply request filter alias 2026-05-04 15:22:23 +02:00
Chirag Aggarwal e81ae67745 fix(specs): align console Cookie description with client/server 2026-05-04 16:53:22 +05:30
Chirag Aggarwal 1e9b364f58 feat(specs): include union of auth headers in every platform's securityDefinitions
Each platform spec previously declared only the auth headers referenced by
endpoints in that platform's surface. The unified web SDK now exposes a
single Client class with cross-platform auth factories (fromSession,
fromCookie, fromDevKey, etc.), which means an SDK generator targeting one
platform still needs setter generation for headers that other platforms
expose. The web SDK currently papers over this with a webClientHeaders
augmentation in the SDK generator.

Add the missing securityDefinitions entries directly so every platform
spec carries the union, removing the need for SDK-side augmentation:

- client: + Cookie (for SSR cookie forwarding)
- server: + Cookie, DevKey
- console: + Session, DevKey

Per-endpoint security requirements continue to be filtered per platform
as before; only the securityDefinitions registry expands to declare
schemes that exist API-wide. SDKs that iterate the registry to emit
auth setters now produce the union without per-SDK fallback code.
2026-05-04 16:50:38 +05:30
Matej Bačo d7d0ecb106 Fix failing tests 2026-05-04 12:17:34 +02:00
Matej Bačo 879dc6873e review fixes 2026-05-04 12:07:52 +02:00
Matej Bačo 2263632f7d Fix all proxy create endpoints 2026-05-04 12:06:37 +02:00
Matej Bačo 2231830826 Further proxy API improvements 2026-05-04 11:59:04 +02:00
Matej Bačo cd6f5c64f0 Improve proxy API quality 2026-05-04 11:48:02 +02:00
Matej Bačo d8349544d2 Revert unwanted change 2026-05-04 11:37:26 +02:00
Matej Bačo ec75f101bb SDK quality fix 2026-05-04 11:37:00 +02:00
Matej Bačo 9c9bde2ce6 Introduce project key console tests 2026-05-04 11:36:54 +02:00
ArnabChatterjee20kandGitHub 554fdf5472 Merge pull request #12206 from appwrite/fix-query-syntax-errors
fix: resolve query syntax errors and improve error handling in Reques…
2026-05-04 12:57:39 +05:30
Damodar LohaniandGitHub 44dd0e8c32 Merge pull request #12207 from appwrite/chore-bump-utopia-audit-2.2.2
chore: bump utopia-php/audit to 2.2.2
2026-05-04 12:58:48 +05:45
Damodar Lohani f27172018f chore: bump utopia-php/audit to 2.2.2 2026-05-04 06:56:56 +00:00
Prem Palanisamy b79637eef5 Re-add onDuplicate param to modular migration endpoints
1.9.x reorganized app/controllers/api/migrations.php into the
Platform/Modules/Migrations structure but dropped the onDuplicate
param. After the merge, every e2e migration test that passed
'onDuplicate' => 'overwrite' would 400 since the param wasn't in the
allowlist anymore.

Restoring it on the three endpoints that take row-level conflict
behavior: Appwrite/Create, CSV/Imports/Create, JSON/Imports/Create.
Each:
  - Imports OnDuplicate + WhiteList.
  - Adds optional ->param('onDuplicate', OnDuplicate::Fail->value,
    new WhiteList(OnDuplicate::values()), …).
  - Threads $onDuplicate through the action signature.
  - Stores it on the migration document's 'options' attribute so
    Workers/Migrations.php can pick it up via
    OnDuplicate::tryFrom($options['onDuplicate'] ?? '')
    ?? OnDuplicate::Fail.

Worker code already reads options['onDuplicate'] (unchanged) — no
edits needed there.
2026-04-30 17:01:57 +01:00
Prem Palanisamy b1906e5eb9 Merge remote-tracking branch 'origin/1.9.x' into feat/skip-duplicates
# Conflicts:
#	app/controllers/api/migrations.php
#	composer.lock
2026-04-30 16:26:43 +01:00
premtsd-codeandGitHub e14db61e30 Merge branch '1.9.x' into backup-migration-multitype 2026-04-30 16:42:11 +05:30
Prem Palanisamy 5ffb7c82e7 Merge 1.9.x into backup-migration-multitype 2026-04-30 11:55:01 +01:00
Prem Palanisamy d4e32af792 Migrate appwrite to OnDuplicate::Overwrite ('overwrite')
Maintainer review on utopia-php/migration#171 renamed
OnDuplicate::Upsert -> OnDuplicate::Overwrite (value 'upsert' ->
'overwrite') to align with Appwrite terms (skip / overwrite / fail).
Applying the cross-repo ripple here:

- app/controllers/api/migrations.php: 3 endpoint param descriptions
  updated ('upsert' -> 'overwrite' in the help text). The validator
  still uses OnDuplicate::values() so it auto-picks up the new value.
- tests/e2e/Services/Migrations/MigrationsBase.php: all
  'onDuplicate' => 'upsert' -> 'overwrite'; method names
  testAppwriteMigrationUpsert* -> testAppwriteMigrationOverwrite*;
  comments / assertion messages / local var names switched.
- Left untouched: utopia's upsertDocuments operation, transaction
  TransactionState 'upsert' action, Operation validator — those refer
  to the database-level upsert primitive, not the OnDuplicate enum.

composer.lock: utopia-php/migration 7d71505 -> b8ae7bc.
2026-04-30 11:45:33 +01:00
Prem Palanisamy 8f32d01686 composer: bump utopia-php/migration to 7d71505 (createIndex pre-check before count/validator) 2026-04-28 08:10:39 +01:00
Prem Palanisamy e1209614c5 composer: bump utopia-php/migration to d37efed (track partner key for orphan cleanup) 2026-04-28 07:56:14 +01:00
Prem Palanisamy 40aa8cb1bd composer: bump utopia-php/migration to 4c3965b (tighten dropAttributeForRecreate) 2026-04-28 07:11:23 +01:00
Prem Palanisamy 4ae4531355 composer: bump utopia-php/migration to aa1e7c7 (drop+recreate fixes) 2026-04-28 07:06:25 +01:00
Prem Palanisamy 443f5cfb0e tests: switch attribute-recreate test to non-SDK change
After dropping createdAt from resolveSchemaAction, source-side recreate
no longer routes through DropAndRecreate via the outer decision. The
inner fallthrough still drops+recreates when the spec diff is a non-SDK
change, so this test now toggles 'array' (a non-SDK field) on recreate
to actually exercise the drop+recreate path it pins.

Also clarifies the two-way recreate test's docblock — with createdAt
gone and identical spec on recreate, it exercises spec-match + pair-key
dedup (both tolerate paths) rather than parent-side drop. End-state
assertions unchanged.
2026-04-28 05:55:38 +01:00
Prem Palanisamy 5a928f2c0f composer: bump utopia-php/migration to 6ec2c45 (drop createdAt) 2026-04-28 05:43:43 +01:00
Prem Palanisamy f5730e8eed composer: bump utopia-php/migration to 36a1acf (spec-match order fix) 2026-04-28 05:16:46 +01:00
Prem Palanisamy b6afef6efc tests: trim verbose multi-paragraph docblocks to one-liners 2026-04-28 04:44:19 +01:00
Prem Palanisamy 3fba7afd2e tests: pin spec-match guard + bump migration to c8d1789
testAppwriteMigrationUpsertSameSpecRecreateTolerates exercises the new
spec-match guard added in utopia-php/migration c8d1789. Source drops +
recreates a column with the EXACT same spec as before; createdAt
advances but specs match → action is forced to Tolerate. Asserts dest
column's $createdAt stays at first-migration value (proving Tolerate,
not DropAndRecreate). Row pass under Upsert still propagates source's
new row value.

Companion to testAppwriteMigrationUpsertAttributeRecreateDropsAndRecreates
which exercises the spec-DIFFERS path: same precondition (drop + recreate),
different outcome (DropAndRecreate vs Tolerate) gated on spec equality.

composer.lock: utopia-php/migration 24fd23b -> c8d1789 (spec-match guard).
2026-04-28 04:16:51 +01:00
Prem Palanisamy ac46ff802a composer: bump utopia-php/migration to 24fd23b (SDK-boundary lock-in test) 2026-04-28 03:45:25 +01:00
Prem Palanisamy 8cccc79935 composer: bump utopia-php/migration to 47933c1 (inline TwoWayPartner) 2026-04-28 03:31:25 +01:00
Prem Palanisamy b03c901fa7 tests: cover one-way DropAndRecreate + attribute-recreate scenarios
Two coverage gaps closed:

- testAppwriteMigrationUpsertOneWayRelationshipDropAndRecreate exercises
  the path that updateRelationshipInPlace gates off: one-way + onDelete
  change → returns false → falls through to DropAndRecreate via
  deleteRelationship. Coverage was lost when
  testAppwriteMigrationUpsertUpdatesRelationshipOnDeleteInPlace was
  converted to two-way to actually hit the in-place path.

- testAppwriteMigrationUpsertAttributeRecreateDropsAndRecreates pins
  the createdAt-different leaf path: source drops + recreates the
  attribute (createdAt advances), re-migration must DropAndRecreate
  on dest and re-flow the row data through the row pass. Companion to
  testAppwriteMigrationUpsertUpdatesAttributeInPlace which covers the
  same-createdAt + newer-updatedAt path.

Migration package already at 09c1b21 (the maintainability commit) from
the previous lock bump — no further composer.lock change needed.
2026-04-27 17:40:23 +01:00
Prem Palanisamy 7b32fd0196 composer: bump utopia-php/migration to 09c1b21 (maintainability pass) 2026-04-27 17:32:14 +01:00
Prem Palanisamy c80c747e48 tests: pin two-way recreate partner-side dedup + bump migration
testAppwriteMigrationUpsertTwoWayRecreateSkipsPartnerSide exercises
the DropAndRecreate path on a two-way relationship that the partner-
side pair-key dedup guards. Source recreates the relationship between
runs, forcing parent-side createdAt diff. Test asserts the migration
completes cleanly and partner-table rows survive — without dedup, the
partner pass re-fires DropAndRecreate and destroys those rows.

composer.lock: utopia-php/migration c76de9a -> c13e77d (partner-side
pair-key dedup restored).
2026-04-27 14:45:39 +01:00
Prem Palanisamy dfde1be035 tests: cover two-way relationship onDelete in-place update + bump migration
The previous version of this test created a one-way relationship,
which falls through to DropAndRecreate (one-way + onDelete change is
gated off in updateRelationshipInPlace because utopia's
updateRelationship partner-cascade throws on one-way). It never
exercised the in-place path it was named for.

Converted to two-way (parents.kids ↔ children.parent), and asserted
both parent- and partner-side onDelete on dest. Partner-side
assertion is the regression guard for the partner-meta refresh that
was missing from updateRelationshipInPlace.

composer.lock: utopia-php/migration a36d95f -> c76de9a (partner-side
onDelete sync fix).
2026-04-27 14:03:02 +01:00
Prem Palanisamy 9be0ed8af0 Merge remote-tracking branch 'origin/feat/skip-duplicates' into feat/skip-duplicates 2026-04-27 12:35:35 +01:00
Prem Palanisamy 5f8a32798d tests: SDK-aligned UpdateInPlace coverage + bump migration to a36d95f
Three new e2e tests in MigrationsBase covering the schema reconciliation
paths added in utopia-php/migration:

- testAppwriteMigrationUpsertUpdatesAttributeInPlace: PATCH source
  required/default (SDK-reachable), assert dest reflects change and the
  pre-existing row's column data is preserved (drop+recreate would have
  wiped it).
- testAppwriteMigrationSkipPreservesAttributeDrift: leaf-level analog
  of the existing container-drift Skip test — guards Skip from ever
  consulting timestamps.
- testAppwriteMigrationUpsertUpdatesRelationshipOnDeleteInPlace: PATCH
  source onDelete cascade->restrict (SDK-reachable), assert dest
  reflects change and structural fields (relationType, twoWay) untouched.

composer.lock: utopia-php/migration 6e6f825 -> a36d95f (mechanical
helpers replacement, parseTimestamp dedup, match dispatch, comment trim).
2026-04-27 12:32:37 +01:00
premtsd-codeandGitHub 557fef2c7b Merge branch '1.9.x' into feat/skip-duplicates 2026-04-27 15:12:17 +05:30
Prem Palanisamy ece468d1f7 Merge remote-tracking branch 'origin/1.9.x' into feat/skip-duplicates 2026-04-27 08:11:56 +01:00
Prem Palanisamy dc9f48c948 composer: bump utopia-php/migration to 6e6f825 (relationships UpdateInPlace) 2026-04-24 15:09:42 +01:00
Prem Palanisamy 478a2a6e86 tests: Upsert drops orphan columns / Skip preserves them
testAppwriteMigrationUpsertDropsOrphanColumn: adds a column directly on
destination (simulating post-rename orphan or dest-only drift), runs
Upsert, asserts the orphan is dropped and source-declared column
survives. Covers the per-table orphan cleanup fired inside
createRecord before rows land.

testAppwriteMigrationSkipKeepsOrphanColumn: same setup, Skip mode.
Asserts the orphan survives, proving the cleanup is correctly gated
to Upsert only.
2026-04-24 07:30:29 +01:00
Prem Palanisamy 8292f7493d composer: bump utopia-php/migration to c2b8715 (rename orphan tracker to 'table' vocabulary) 2026-04-24 07:25:37 +01:00
Prem Palanisamy e728265d21 composer: bump utopia-php/migration to 36fdf26 (Upsert orphan cleanup + nullable timestamps) 2026-04-24 05:57:32 +01:00
Prem Palanisamy 84b6dfa9d5 composer: bump utopia-php/migration to bb21912 (SKIPPED status on tolerate) 2026-04-23 16:39:30 +01:00
Prem Palanisamy 0a26608155 Merge branch 'feat/skip-duplicates' of https://github.com/appwrite/appwrite into feat/skip-duplicates 2026-04-23 07:15:36 +01:00
Prem Palanisamy 18694e0a28 TEMP: run Migrations across all adapters/modes on this branch 2026-04-23 07:10:23 +01:00
premtsd-codeandGitHub 07307c6131 Merge branch '1.9.x' into feat/skip-duplicates 2026-04-23 11:22:05 +05:30
Prem Palanisamy 0134832fb6 tests: cover Upsert UpdateInPlace for database/table + Skip preserves dest drift
Two new E2E tests exercising the schema-tolerance UpdateInPlace path
added in utopia-php/migration's DestinationAppwrite.

testAppwriteMigrationUpsertUpdatesContainerMetadata (positive):
- Fresh migration copies source database + table + column + row to dest.
- Mutates source database name (PUT /databases/:id) and table
  name/permissions/rowSecurity/enabled (PUT /tablesdb/:db/tables/:id).
- One-second sleep before mutation ensures source's $updatedAt is
  strictly greater than dest's at second granularity (strtotime
  comparison).
- Upsert re-migration asserts:
  - 'completed' status.
  - dest database name matches source's new name.
  - dest table name / enabled / rowSecurity match source's new values.
  - child row's 'name' attribute is untouched — UpdateInPlace only
    rewrites container metadata, not rows.

testAppwriteMigrationSkipPreservesContainerDrift (negative):
- Fresh migration, then mutate BOTH dest (simulating ops tightening
  permissions post-migration) and source (divergence).
- Skip re-migration asserts dest kept its tightened values — Skip's
  strict "don't touch" contract protects dev→prod cutover workflows
  from accidentally wiping ops-side drift on schema re-sync.

Both tests use performMigrationSync for strict 'completed' assertions.
Runtime ~18s combined. Existing testAppwriteMigrationRowsOnDuplicate
and testAppwriteMigrationReRunIsIdempotent regression-tested locally.
2026-04-23 06:22:26 +01:00
Prem Palanisamy 44b7b26adf composer: bump utopia-php/migration to e08d531
Picks up the UpdateInPlace branch — database/table metadata drift is
now reconciled on Upsert-newer (renames, enable toggles, table
permissions / documentSecurity) via updateDocument, without touching
child rows.
2026-04-23 06:06:30 +01:00
Prem Palanisamy c1506643e9 composer: bump utopia-php/migration to cfba224
Picks up the PR #171 refactor + unit tests:
- resolveSchemaAction decision point consolidation
- deleteAttributeCompletely primitive (two-way cleanup in one place)
- Hardened sourceIsNewer against MySQL zero-date sentinel
- 14 unit tests locking the decision matrix
2026-04-23 00:12:09 +01:00
Prem Palanisamy 2ad95e5113 ci: restrict to Migrations service for fast feedback loop (TEMP)
Branch is iterating on utopia-php/migration's re-migration tolerance.
Other test matrices (unit, general, abuse, screenshots, benchmark, and
every other e2e service) add ~30+ minutes to CI without exercising code
this PR touches. Restrict the matrix to the Migrations service and skip
the unrelated test jobs until the migration work is ready to merge.

All jobs marked with 'TEMP:' comments + 'if: false' — revert to the
full matrix before merging to main.

Static analysis (lint, phpstan, composer audit, specs, locale, security)
still runs on every PR push.
2026-04-22 16:46:33 +01:00
Prem Palanisamy 715bd40b4a Tighten A→A re-migration tests against utopia-php/migration schema tolerance
utopia-php/migration's DestinationAppwrite now handles schema tolerance on
re-migration (PR #171 on feat/skip-duplicates): it pre-checks destination
`_metadata` for each database / table / column / index and tolerates in
Skip/Upsert mode. Re-runs no longer produce schema-level errors, so the
E2E tests can drop the status-tolerant workaround and assert strict
'completed' outcomes.

Changes:

- composer.json: pin utopia-php/migration to dev-feat/skip-duplicates (aliased
  to 1.9.99 for stability resolution). Will be replaced with a fixed 1.10.0
  tag once the migration PR lands.

- testAppwriteMigrationRowsOnDuplicate: replace the tolerant
  runMigrationAssertingRowSuccess helper with performMigrationSync on the
  Skip and Upsert re-runs. Asserts 'completed' status on every run,
  destination row content matches the expected value per mode (Mutated
  preserved on Skip, Original restored on Upsert). Helper method removed.

- testAppwriteMigrationReRunIsIdempotent (new): seeds two rows on source,
  runs the migration three times back-to-back (fresh, Skip re-run, Upsert
  re-run) against unchanged source data, asserts strict 'completed' on every
  run and row content is stable across all three. Exercises the
  schema-tolerance path end-to-end: every database/table/column on
  destination already exists with a matching spec, so DestinationAppwrite's
  pre-check returns Tolerate for every resource.
2026-04-22 16:38:32 +01:00
Prem Palanisamy 19f02a5129 composer: sync lock content-hash after merge from 1.9.x 2026-04-20 16:44:36 +01:00
Prem Palanisamy d0603c4d28 tests: add Appwrite->Appwrite row onDuplicate test with tolerant poller 2026-04-20 16:38:59 +01:00
premtsd-codeandGitHub 187d0c0cf4 Merge branch '1.9.x' into feat/skip-duplicates 2026-04-20 19:41:01 +05:30
Prem Palanisamy fc6bd7232e tests: add Appwrite->Appwrite row migration onDuplicate test 2026-04-20 14:50:57 +01:00
Prem Palanisamy e878b0b403 tests: cover onDuplicate on JSON import endpoint + column readiness wait 2026-04-20 14:43:45 +01:00
Prem Palanisamy f3c2502a8c composer: widen database/migration pins to minor-version wildcards 2026-04-20 13:57:44 +01:00
Prem Palanisamy 18f9dfb64e Bump utopia-php/migration to 1.9.2, use OnDuplicate enum 2026-04-20 13:16:45 +01:00
Prem Palanisamy 8fa28257be Migrations: replace overwrite/skip with onDuplicate enum string, bump database to 5.3.22 2026-04-20 10:28:05 +01:00
ArnabChatterjee20k e7072574ad updated tests 2026-04-16 14:13:01 +05:30
ArnabChatterjee20k d84f71e728 updated 2026-04-16 14:10:20 +05:30
ArnabChatterjee20k 5f41f7fb86 Merge remote-tracking branch 'origin/1.9.x' into big-int 2026-04-16 14:07:49 +05:30
Prem Palanisamy c5fe71684a Add E2E tests for CSV import overwrite/skip flags
Three new test methods in MigrationsBase, following the existing
testCreateCSVImport setup pattern:

- testCreateCSVImportSkipDuplicates
  Seeds documents.csv, mutates one row, re-imports with skip=true.
  Asserts the mutated row keeps its mutated value (not overwritten
  by the CSV's original value) and the row count stays at 100.

- testCreateCSVImportOverwrite
  Seeds documents.csv, mutates one row, re-imports with overwrite=true.
  Asserts the mutated row is restored to the CSV's original value
  (proving upsertDocuments actually replaced the row) and the row
  count stays at 100.

- testCreateCSVImportDefaultFailsOnDuplicate
  Regression guard: re-imports documents.csv with no flags. Asserts
  the migration goes to status=failed with errors populated, proving
  the default duplicate-throws behavior is preserved.

All three share a prepareCsvImportFixture() helper that sets up
database + table (name, age columns) + bucket + documents.csv
upload. Returns the known first-row id + original name/age so tests
can mutate and assert on a predictable row.

Reuses the existing documents.csv fixture (100 rows with \$id as the
first column). No new fixture files needed.
2026-04-15 14:23:02 +01:00
Prem Palanisamy d44820e00c Add overwrite and skip options to migration imports
Exposes two new optional boolean params on the three migration
creation endpoints so CSV / JSON / appwrite-to-appwrite imports can
choose how to handle rows whose IDs already exist at the destination.

Endpoints updated (app/controllers/api/migrations.php):
- POST /v1/migrations/appwrite
- POST /v1/migrations/csv/imports
- POST /v1/migrations/json/imports

Parameter semantics:
- overwrite=true  -> destination uses upsertDocuments instead of
                     createDocuments; existing rows are replaced
                     with imported values
- skip=true       -> destination wraps createDocuments in
                     skipDuplicates; existing rows are preserved
                     unchanged, duplicate-id rows silently no-op
- both false      -> default; fails fast on DuplicateException
                     (original behavior, unchanged)
- both true       -> overwrite wins (upsert subsumes skip)

Both params are stored in the migration Document's options array
(matches the existing pattern for destination behavior config like
path, size, delimiter, bucketId, etc.) and read back in the worker's
processDestination() to instantiate DestinationAppwrite with the
new constructor params.

Feature-branch note: depends on utopia-php/migration#feat/skip-duplicates
(DestinationAppwrite constructor params) which in turn depends on
utopia-php/database#852 (skipDuplicates scope guard). composer.json is
temporarily pinned to dev-feat/skip-duplicates and
dev-csv-import-upsert-v2 respectively; both must be reset to proper
release versions once the upstream PRs merge.
2026-04-15 14:15:08 +01:00
ArnabChatterjee20k b3c6e0e67f removed size from the conditions 2026-04-09 17:10:47 +05:30
ArnabChatterjee20k 5c0c0b44c0 Merge remote-tracking branch 'origin/big-int' into big-int 2026-04-09 17:08:09 +05:30
ArnabChatterjee20k 4f599fc75c updated 2026-04-09 17:07:47 +05:30
ArnabChatterjee20kandGitHub 928aa9c909 Merge branch '1.9.x' into big-int 2026-04-09 14:43:51 +05:30
ArnabChatterjee20k 0ea8379065 added the changes 2026-04-09 14:11:21 +05:30
ArnabChatterjee20k 0c734f8f12 updated 2026-04-09 12:02:13 +05:30
ArnabChatterjee20k 229ab28427 Merge remote-tracking branch 'origin/1.9.x' into big-int 2026-04-09 11:15:59 +05:30
ArnabChatterjee20k 45dc259df3 updated validator 2026-04-09 11:14:28 +05:30
ArnabChatterjee20k 71b28eae5a updated db 2026-04-01 15:55:26 +05:30
ArnabChatterjee20k 9cb2ea1135 Merge remote-tracking branch 'origin/bump-database-version-bkp' into big-int 2026-04-01 15:54:56 +05:30
premtsd-codeandGitHub d38c921781 Merge branch '1.9.x' into bump-database-version 2026-04-01 05:23:37 +01:00
ArnabChatterjee20k 3c20ea47f5 Merge remote-tracking branch 'origin/1.9.x' into big-int 2026-04-01 09:51:07 +05:30
Prem Palanisamy 32728ae4a0 Merge remote-tracking branch 'origin/1.9.x' into bump-database-version 2026-04-01 02:16:20 +02:00
ArnabChatterjee20k ad7f83db11 updated actions 2026-03-30 20:32:03 +05:30
ArnabChatterjee20k c4b3956bef emtpy 2026-03-30 20:26:23 +05:30
ArnabChatterjee20k 5fa4551400 Refactor bigint handling in validation and action classes to unify range validation logic 2026-03-30 20:10:13 +05:30
ArnabChatterjee20k 2fb54e0846 Add BigInt support with dedicated bigintRange format and update related logic 2026-03-30 18:59:24 +05:30
ArnabChatterjee20k dfbf22cf3c updated format 2026-03-30 18:36:19 +05:30
ArnabChatterjee20k a5b2021d1e empty 2026-03-30 18:17:14 +05:30
ArnabChatterjee20kandGitHub fb28b31906 Merge branch '1.9.x' into big-int 2026-03-30 18:02:29 +05:30
ArnabChatterjee20k 737bfc599a fixed analyze 2026-03-30 18:00:43 +05:30
ArnabChatterjee20k 819863559e Enhance BigInt support across database attributes and validation, updating types and tests for consistency. 2026-03-30 17:45:25 +05:30
ArnabChatterjee20k f6cdcfb823 Merge remote-tracking branch 'origin/big-int' into big-int 2026-03-30 17:39:25 +05:30
ArnabChatterjee20k e5d818ba44 updated 2026-03-30 17:38:24 +05:30
ArnabChatterjee20kGitHubgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
19487cf56b Update src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/BigInt/Create.php
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-30 17:34:36 +05:30
ArnabChatterjee20k 5741041f33 updated tests 2026-03-30 17:32:49 +05:30
ArnabChatterjee20k 89eb5c2254 Refactor response model attribute and column types to prioritize BigInt over Integer for more accurate type matching in AttributeList, Collection, ColumnList, and Table classes. 2026-03-30 17:14:38 +05:30
ArnabChatterjee20k 77d195f0c3 updated response models 2026-03-30 16:17:44 +05:30
ArnabChatterjee20k a9ed74c6a8 added tests to migration 2026-03-30 16:11:42 +05:30
ArnabChatterjee20k 01db1efbc4 added tests 2026-03-30 16:08:21 +05:30
ArnabChatterjee20k ffa3f741ed added bigint columns 2026-03-30 15:53:06 +05:30
ArnabChatterjee20k 03c73ac0aa added big int attributes 2026-03-30 15:50:59 +05:30
premtsd-codeandGitHub 28882a4414 Merge branch '1.9.x' into backup-migration-multitype 2026-03-25 06:57:37 +00:00
Prem Palanisamy d993214810 Revert "Add policies.read/write scopes to test API key"
This reverts commit 2838642bae.
2026-03-24 17:22:31 +00:00
Prem Palanisamy 2838642bae Add policies.read/write scopes to test API key 2026-03-24 17:09:27 +00:00
Prem Palanisamy bd03ec7a50 Update composer.lock for migration feature branch 2026-03-24 13:36:42 +00:00
Prem Palanisamy 6128a049f2 Point migration dep to feature branch 2026-03-24 13:33:36 +00:00
premtsd-codeandGitHub 9ba6bfba62 Merge branch '1.9.x' into backup-migration-multitype 2026-03-24 13:13:23 +00:00
premtsd-codeandGitHub 6f73602dd7 Merge branch '1.9.x' into backup-migration-multitype 2026-03-24 13:01:29 +00:00
Prem Palanisamy b1e97b4539 Merge 1.9.x and add new API key scopes 2026-03-24 12:14:50 +00:00
Prem Palanisamy 065a0fddae Add backup policy migration support 2026-03-12 13:41:00 +00:00
105 changed files with 5771 additions and 1437 deletions
+7
View File
@@ -1523,6 +1523,13 @@ return [
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_team_confirm'),
'type' => Database::INDEX_KEY,
'attributes' => ['teamInternalId', 'confirm'],
'lengths' => [],
'orders' => [],
],
],
],
+22 -1
View File
@@ -404,6 +404,13 @@ $platformCollections = [
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_teamInternalId'),
'type' => Database::INDEX_KEY,
'attributes' => ['teamInternalId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
],
],
@@ -635,6 +642,13 @@ $platformCollections = [
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_project_id'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
],
],
@@ -1007,7 +1021,14 @@ $platformCollections = [
'attributes' => ['projectInternalId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
]
],
[
'$id' => ID::custom('_key_project_id'),
'type' => Database::INDEX_KEY,
'attributes' => ['projectId'],
'lengths' => [Database::LENGTH_KEY],
'orders' => [Database::ORDER_ASC],
],
],
],
+10
View File
@@ -623,6 +623,11 @@ return [
'description' => 'Synchronous function execution timed out. Use asynchronous execution instead, or ensure the execution duration doesn\'t exceed 30 seconds.',
'code' => 408,
],
Exception::FUNCTION_ASYNCHRONOUS_TIMEOUT => [
'name' => Exception::FUNCTION_ASYNCHRONOUS_TIMEOUT,
'description' => 'Asynchronous function execution timed out. Ensure the execution duration doesn\'t exceed the configured function timeout.',
'code' => 408,
],
Exception::FUNCTION_TEMPLATE_NOT_FOUND => [
'name' => Exception::FUNCTION_TEMPLATE_NOT_FOUND,
'description' => 'Function Template with the requested ID could not be found.',
@@ -687,6 +692,11 @@ return [
'description' => 'Build with the requested ID failed. Please check the logs for more information.',
'code' => 400,
],
Exception::BUILD_TIMEOUT => [
'name' => Exception::BUILD_TIMEOUT,
'description' => 'Build timed out. Increase the build timeout via the `_APP_COMPUTE_BUILD_TIMEOUT` environment variable, or simplify the build to complete within the limit.',
'code' => 408,
],
/** Deployments */
Exception::DEPLOYMENT_NOT_FOUND => [
+2 -2
View File
@@ -59,8 +59,8 @@ $admins = [
'oauth2.write',
'mocks.read',
'mocks.write',
'policies.read',
'policies.write',
'project.policies.read',
'project.policies.write',
'templates.read',
'templates.write',
'projects.write',
+23 -9
View File
@@ -44,11 +44,23 @@ return [
"category" => "Project",
],
"policies.read" => [
"description" =>
"Access to read project\'s policies. Replaced by \'project.policies.read\' for more granular control",
"category" => "Project",
'deprecated' => true,
],
"policies.write" => [
"description" =>
"Access to update project\'s policies. Replaces by \'project.policies.write\' for more granular control",
"category" => "Project",
'deprecated' => true,
],
"project.policies.read" => [
"description" =>
"Access to read project\'s policies",
"category" => "Project",
],
"policies.write" => [
"project.policies.write" => [
"description" =>
"Access to update project\'s policies",
"category" => "Project",
@@ -286,6 +298,16 @@ return [
'category' => 'Messaging',
],
// Proxy
'rules.read' => [
'description' => 'Access to read proxy rules.',
'category' => 'Proxy',
],
'rules.write' => [
'description' => 'Access to create, update, and delete proxy rules.',
'category' => 'Proxy',
],
// Other
"webhooks.read" => [
"description" =>
@@ -339,12 +361,4 @@ return [
'description' => 'Access to create, update, and delete resources under VCS service.',
'category' => 'Other',
],
'rules.read' => [
'description' => 'Access to read proxy rules.',
'category' => 'Other',
],
'rules.write' => [
'description' => 'Access to create, update, and delete proxy rules.',
'category' => 'Other',
],
];
+2 -2
View File
@@ -830,11 +830,11 @@ Http::patch('/v1/account/sessions/:sessionId')
$refreshToken = $session->getAttribute('providerRefreshToken', '');
$oAuthProviders = Config::getParam('oAuthProviders') ?? [];
$className = $oAuthProviders[$provider]['class'] ?? null;
if (!empty($provider) && ($className === null || !\class_exists($className))) {
if (!empty($refreshToken) && ($className === null || !\class_exists($className))) {
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
}
if (!empty($provider) && \class_exists($className)) {
if ($className !== null && \class_exists($className)) {
$appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
$appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}';
+33 -20
View File
@@ -28,6 +28,7 @@ use Appwrite\Utopia\Request\Filters\V21 as RequestV21;
use Appwrite\Utopia\Request\Filters\V22 as RequestV22;
use Appwrite\Utopia\Request\Filters\V23 as RequestV23;
use Appwrite\Utopia\Request\Filters\V24 as RequestV24;
use Appwrite\Utopia\Request\Filters\V25 as RequestV25;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
use Appwrite\Utopia\Response\Filters\V17 as ResponseV17;
@@ -38,7 +39,9 @@ use Appwrite\Utopia\Response\Filters\V21 as ResponseV21;
use Appwrite\Utopia\Response\Filters\V22 as ResponseV22;
use Appwrite\Utopia\Response\Filters\V23 as ResponseV23;
use Appwrite\Utopia\Response\Filters\V24 as ResponseV24;
use Appwrite\Utopia\Response\Filters\V25 as ResponseV25;
use Appwrite\Utopia\View;
use Executor\Exception\Timeout as ExecutorTimeout;
use Executor\Executor;
use MaxMind\Db\Reader;
use Swoole\Http\Request as SwooleRequest;
@@ -579,26 +582,30 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S
'site' => '',
};
$executionResponse = $executor->createExecution(
projectId: $project->getId(),
deploymentId: $deployment->getId(),
body: \strlen($body) > 0 ? $body : null,
variables: $vars,
timeout: $resource->getAttribute('timeout', 30),
image: $runtime['image'],
source: $source,
entrypoint: $entrypoint,
version: $version,
path: $path,
method: $method,
headers: $headers,
runtimeEntrypoint: $runtimeEntrypoint,
cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT,
memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT,
logging: $resource->getAttribute('logging', true),
requestTimeout: 30,
responseFormat: Executor::RESPONSE_FORMAT_ARRAY_HEADERS
);
try {
$executionResponse = $executor->createExecution(
projectId: $project->getId(),
deploymentId: $deployment->getId(),
body: \strlen($body) > 0 ? $body : null,
variables: $vars,
timeout: $resource->getAttribute('timeout', 30),
image: $runtime['image'],
source: $source,
entrypoint: $entrypoint,
version: $version,
path: $path,
method: $method,
headers: $headers,
runtimeEntrypoint: $runtimeEntrypoint,
cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT,
memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT,
logging: $resource->getAttribute('logging', true),
requestTimeout: 30,
responseFormat: Executor::RESPONSE_FORMAT_ARRAY_HEADERS
);
} catch (ExecutorTimeout $th) {
throw new AppwriteException(AppwriteException::FUNCTION_SYNCHRONOUS_TIMEOUT, previous: $th);
}
$headerOverrides = [];
@@ -904,6 +911,9 @@ Http::init()
if (version_compare($requestFormat, '1.9.3', '<')) {
$request->addFilter(new RequestV24());
}
if (version_compare($requestFormat, '1.9.4', '<')) {
$request->addFilter(new RequestV25());
}
}
$localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
@@ -928,6 +938,9 @@ Http::init()
*/
$responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
if ($responseFormat) {
if (version_compare($responseFormat, '1.9.4', '<')) {
$response->addFilter(new ResponseV25());
}
if (version_compare($responseFormat, '1.9.3', '<')) {
$response->addFilter(new ResponseV24());
}
+3 -2
View File
@@ -44,14 +44,15 @@ const APP_PROJECT_ACCESS = 24 * 60 * 60; // 24 hours
const APP_RESOURCE_TOKEN_ACCESS = 24 * 60 * 60; // 24 hours
const APP_FILE_ACCESS = 24 * 60 * 60; // 24 hours
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
const APP_CACHE_BUSTER = 4324;
const APP_VERSION_STABLE = '1.9.3';
const APP_CACHE_BUSTER = 4325;
const APP_VERSION_STABLE = '1.9.4';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
const APP_DATABASE_ATTRIBUTE_DATETIME = 'datetime';
const APP_DATABASE_ATTRIBUTE_URL = 'url';
const APP_DATABASE_ATTRIBUTE_INT_RANGE = 'intRange';
const APP_DATABASE_ATTRIBUTE_BIGINT_RANGE = 'bigintRange';
const APP_DATABASE_ATTRIBUTE_FLOAT_RANGE = 'floatRange';
const APP_DATABASE_ATTRIBUTE_POINT = 'point';
const APP_DATABASE_ATTRIBUTE_LINE = 'line';
+7
View File
@@ -36,6 +36,13 @@ Structure::addFormat(APP_DATABASE_ATTRIBUTE_INT_RANGE, function ($attribute) {
return new Range($min, $max, Range::TYPE_INTEGER);
}, Database::VAR_INTEGER);
// BigInt uses a dedicated bigintRange format name to avoid clobbering `intRange`.
Structure::addFormat(APP_DATABASE_ATTRIBUTE_BIGINT_RANGE, function ($attribute) {
$min = $attribute['formatOptions']['min'] ?? -INF;
$max = $attribute['formatOptions']['max'] ?? INF;
return new Range($min, $max, Range::TYPE_INTEGER);
}, Database::VAR_BIGINT);
Structure::addFormat(APP_DATABASE_ATTRIBUTE_FLOAT_RANGE, function ($attribute) {
$min = $attribute['formatOptions']['min'] ?? -INF;
$max = $attribute['formatOptions']['max'] ?? INF;
+4
View File
@@ -11,6 +11,7 @@ use Appwrite\Utopia\Response\Model\AlgoScryptModified;
use Appwrite\Utopia\Response\Model\AlgoSha;
use Appwrite\Utopia\Response\Model\Any;
use Appwrite\Utopia\Response\Model\Attribute;
use Appwrite\Utopia\Response\Model\AttributeBigInt;
use Appwrite\Utopia\Response\Model\AttributeBoolean;
use Appwrite\Utopia\Response\Model\AttributeDatetime;
use Appwrite\Utopia\Response\Model\AttributeEmail;
@@ -37,6 +38,7 @@ use Appwrite\Utopia\Response\Model\Branch;
use Appwrite\Utopia\Response\Model\Bucket;
use Appwrite\Utopia\Response\Model\Collection;
use Appwrite\Utopia\Response\Model\Column;
use Appwrite\Utopia\Response\Model\ColumnBigInt;
use Appwrite\Utopia\Response\Model\ColumnBoolean;
use Appwrite\Utopia\Response\Model\ColumnDatetime;
use Appwrite\Utopia\Response\Model\ColumnEmail;
@@ -297,6 +299,7 @@ Response::setModel(new Attribute());
Response::setModel(new AttributeList());
Response::setModel(new AttributeString());
Response::setModel(new AttributeInteger());
Response::setModel(new AttributeBigInt());
Response::setModel(new AttributeFloat());
Response::setModel(new AttributeBoolean());
Response::setModel(new AttributeEmail());
@@ -330,6 +333,7 @@ Response::setModel(new Column());
Response::setModel(new ColumnList());
Response::setModel(new ColumnString());
Response::setModel(new ColumnInteger());
Response::setModel(new ColumnBigInt());
Response::setModel(new ColumnFloat());
Response::setModel(new ColumnBoolean());
Response::setModel(new ColumnEmail());
+4 -4
View File
@@ -74,13 +74,13 @@
"utopia-php/locale": "0.8.*",
"utopia-php/logger": "0.6.*",
"utopia-php/messaging": "0.22.*",
"utopia-php/migration": "1.9.*",
"utopia-php/migration": "1.*",
"utopia-php/platform": "0.13.*",
"utopia-php/pools": "1.*",
"utopia-php/span": "1.1.*",
"utopia-php/preloader": "0.2.*",
"utopia-php/queue": "0.17.*",
"utopia-php/servers": "0.3.*",
"utopia-php/queue": "0.18.*",
"utopia-php/servers": "0.4.*",
"utopia-php/registry": "0.5.*",
"utopia-php/storage": "2.*",
"utopia-php/system": "0.10.*",
@@ -93,7 +93,7 @@
"chillerlan/php-qrcode": "4.3.*",
"adhocore/jwt": "1.1.*",
"spomky-labs/otphp": "11.*",
"webonyx/graphql-php": "15.31.*",
"webonyx/graphql-php": "15.32.*",
"league/csv": "9.14.*",
"enshrined/svg-sanitize": "0.22.*"
},
Generated
+107 -106
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "bd45829c252971301370d62300be106d",
"content-hash": "ec2ad489c60f0102f0dfab223b6d1fe4",
"packages": [
{
"name": "adhocore/jwt",
@@ -2708,16 +2708,16 @@
},
{
"name": "symfony/http-client",
"version": "v7.4.8",
"version": "v7.4.9",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
"reference": "01933e626c3de76bea1e22641e205e78f6a34342"
"reference": "7e941c6abf4e3bf7dca160bf0e11ef36a9f832f6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/01933e626c3de76bea1e22641e205e78f6a34342",
"reference": "01933e626c3de76bea1e22641e205e78f6a34342",
"url": "https://api.github.com/repos/symfony/http-client/zipball/7e941c6abf4e3bf7dca160bf0e11ef36a9f832f6",
"reference": "7e941c6abf4e3bf7dca160bf0e11ef36a9f832f6",
"shasum": ""
},
"require": {
@@ -2785,7 +2785,7 @@
"http"
],
"support": {
"source": "https://github.com/symfony/http-client/tree/v7.4.8"
"source": "https://github.com/symfony/http-client/tree/v7.4.9"
},
"funding": [
{
@@ -2805,7 +2805,7 @@
"type": "tidelift"
}
],
"time": "2026-03-30T12:55:43+00:00"
"time": "2026-04-29T13:25:15+00:00"
},
{
"name": "symfony/http-client-contracts",
@@ -3502,16 +3502,16 @@
},
{
"name": "utopia-php/audit",
"version": "2.2.1",
"version": "2.2.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/audit.git",
"reference": "e3e2d6ad5c7f6377d9237df296a12eb7943892fd"
"reference": "90886c202e7983999e6b6a8201004d5ab61d4b57"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/e3e2d6ad5c7f6377d9237df296a12eb7943892fd",
"reference": "e3e2d6ad5c7f6377d9237df296a12eb7943892fd",
"url": "https://api.github.com/repos/utopia-php/audit/zipball/90886c202e7983999e6b6a8201004d5ab61d4b57",
"reference": "90886c202e7983999e6b6a8201004d5ab61d4b57",
"shasum": ""
},
"require": {
@@ -3545,9 +3545,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/audit/issues",
"source": "https://github.com/utopia-php/audit/tree/2.2.1"
"source": "https://github.com/utopia-php/audit/tree/2.2.2"
},
"time": "2026-02-02T10:39:25+00:00"
"time": "2026-05-04T06:48:58+00:00"
},
{
"name": "utopia-php/auth",
@@ -3658,21 +3658,21 @@
},
{
"name": "utopia-php/cli",
"version": "0.23.2",
"version": "0.23.3",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/cli.git",
"reference": "145b91fef827853bcceaa3ab8ca2b1d6faaca2ab"
"reference": "3c45ae5bcdcd3c7916e1909d74c60b8e771610db"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/cli/zipball/145b91fef827853bcceaa3ab8ca2b1d6faaca2ab",
"reference": "145b91fef827853bcceaa3ab8ca2b1d6faaca2ab",
"url": "https://api.github.com/repos/utopia-php/cli/zipball/3c45ae5bcdcd3c7916e1909d74c60b8e771610db",
"reference": "3c45ae5bcdcd3c7916e1909d74c60b8e771610db",
"shasum": ""
},
"require": {
"php": ">=7.4",
"utopia-php/servers": "0.3.*"
"utopia-php/servers": "0.4.0"
},
"require-dev": {
"laravel/pint": "1.2.*",
@@ -3703,9 +3703,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/cli/issues",
"source": "https://github.com/utopia-php/cli/tree/0.23.2"
"source": "https://github.com/utopia-php/cli/tree/0.23.3"
},
"time": "2026-04-27T09:19:04+00:00"
"time": "2026-05-05T04:38:59+00:00"
},
{
"name": "utopia-php/compression",
@@ -3850,22 +3850,23 @@
},
{
"name": "utopia-php/database",
"version": "5.4.1",
"version": "5.7.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "688d9422b5ff42ac2ecc29397d94891cfd772e93"
"reference": "eb35e68f7f90932d5a60bd72e70158ae7a4e0511"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/688d9422b5ff42ac2ecc29397d94891cfd772e93",
"reference": "688d9422b5ff42ac2ecc29397d94891cfd772e93",
"url": "https://api.github.com/repos/utopia-php/database/zipball/eb35e68f7f90932d5a60bd72e70158ae7a4e0511",
"reference": "eb35e68f7f90932d5a60bd72e70158ae7a4e0511",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"ext-mongodb": "*",
"ext-pdo": "*",
"ext-redis": "*",
"php": ">=8.4",
"utopia-php/cache": "1.*",
"utopia-php/console": "0.1.*",
@@ -3903,9 +3904,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/5.4.1"
"source": "https://github.com/utopia-php/database/tree/5.7.0"
},
"time": "2026-04-29T07:32:59+00:00"
"time": "2026-05-06T01:04:08+00:00"
},
{
"name": "utopia-php/detector",
@@ -4271,23 +4272,23 @@
},
{
"name": "utopia-php/http",
"version": "0.34.24",
"version": "0.34.25",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/http.git",
"reference": "d1eced0627c5a9fceddf53992ed97d664b810d33"
"reference": "76be330d4197bae680eb4ccc29c573456fe91904"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/http/zipball/d1eced0627c5a9fceddf53992ed97d664b810d33",
"reference": "d1eced0627c5a9fceddf53992ed97d664b810d33",
"url": "https://api.github.com/repos/utopia-php/http/zipball/76be330d4197bae680eb4ccc29c573456fe91904",
"reference": "76be330d4197bae680eb4ccc29c573456fe91904",
"shasum": ""
},
"require": {
"php": ">=8.3",
"utopia-php/compression": "0.1.*",
"utopia-php/di": "0.3.*",
"utopia-php/servers": "0.3.*",
"utopia-php/servers": "0.4.0",
"utopia-php/telemetry": "0.2.*",
"utopia-php/validators": "0.2.*"
},
@@ -4321,9 +4322,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/http/issues",
"source": "https://github.com/utopia-php/http/tree/0.34.24"
"source": "https://github.com/utopia-php/http/tree/0.34.25"
},
"time": "2026-04-24T12:16:53+00:00"
"time": "2026-05-05T04:39:15+00:00"
},
{
"name": "utopia-php/image",
@@ -4530,16 +4531,16 @@
},
{
"name": "utopia-php/migration",
"version": "1.9.5",
"version": "1.10.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/migration.git",
"reference": "952a4dfe232702f80e45c35129466a8d8cb4c599"
"reference": "55f4863d690e775f44fec3cae4bd1f4491fed5ea"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/952a4dfe232702f80e45c35129466a8d8cb4c599",
"reference": "952a4dfe232702f80e45c35129466a8d8cb4c599",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/55f4863d690e775f44fec3cae4bd1f4491fed5ea",
"reference": "55f4863d690e775f44fec3cae4bd1f4491fed5ea",
"shasum": ""
},
"require": {
@@ -4579,9 +4580,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/migration/issues",
"source": "https://github.com/utopia-php/migration/tree/1.9.5"
"source": "https://github.com/utopia-php/migration/tree/1.10.0"
},
"time": "2026-04-29T11:19:13+00:00"
"time": "2026-05-06T04:35:32+00:00"
},
{
"name": "utopia-php/mongo",
@@ -4646,26 +4647,26 @@
},
{
"name": "utopia-php/platform",
"version": "0.13.0",
"version": "0.13.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/platform.git",
"reference": "d23af5349a7ea9ee11f9920a13626226f985522e"
"reference": "a20cb8b20a1e4c9886309c2d033a0292ba0937b9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/platform/zipball/d23af5349a7ea9ee11f9920a13626226f985522e",
"reference": "d23af5349a7ea9ee11f9920a13626226f985522e",
"url": "https://api.github.com/repos/utopia-php/platform/zipball/a20cb8b20a1e4c9886309c2d033a0292ba0937b9",
"reference": "a20cb8b20a1e4c9886309c2d033a0292ba0937b9",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-redis": "*",
"php": ">=8.1",
"utopia-php/cli": "0.23.*",
"utopia-php/http": "0.34.*",
"utopia-php/queue": "0.17.*",
"utopia-php/servers": "0.3.*"
"php": ">=8.3",
"utopia-php/cli": "0.23.3",
"utopia-php/http": "0.34.25",
"utopia-php/queue": "0.18.2",
"utopia-php/servers": "0.4.0"
},
"require-dev": {
"laravel/pint": "1.2.*",
@@ -4691,9 +4692,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/platform/issues",
"source": "https://github.com/utopia-php/platform/tree/0.13.0"
"source": "https://github.com/utopia-php/platform/tree/0.13.2"
},
"time": "2026-04-17T09:57:18+00:00"
"time": "2026-05-05T06:00:26+00:00"
},
{
"name": "utopia-php/pools",
@@ -4803,25 +4804,24 @@
},
{
"name": "utopia-php/queue",
"version": "0.17.0",
"version": "0.18.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/queue.git",
"reference": "0fbc7d7312f5cf76ec112513fb93317000901f5f"
"reference": "f85ca003c99ff475708c05466643d067403c0c22"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/queue/zipball/0fbc7d7312f5cf76ec112513fb93317000901f5f",
"reference": "0fbc7d7312f5cf76ec112513fb93317000901f5f",
"url": "https://api.github.com/repos/utopia-php/queue/zipball/f85ca003c99ff475708c05466643d067403c0c22",
"reference": "f85ca003c99ff475708c05466643d067403c0c22",
"shasum": ""
},
"require": {
"php": ">=8.3",
"php-amqplib/php-amqplib": "^3.7",
"utopia-php/di": "0.3.*",
"utopia-php/fetch": "0.5.*",
"utopia-php/pools": "1.*",
"utopia-php/servers": "0.3.*",
"utopia-php/servers": "0.4.0",
"utopia-php/telemetry": "0.2.*",
"utopia-php/validators": "0.2.*"
},
@@ -4864,9 +4864,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/queue/issues",
"source": "https://github.com/utopia-php/queue/tree/0.17.0"
"source": "https://github.com/utopia-php/queue/tree/0.18.2"
},
"time": "2026-03-23T16:21:31+00:00"
"time": "2026-05-05T04:38:59+00:00"
},
{
"name": "utopia-php/registry",
@@ -4922,16 +4922,16 @@
},
{
"name": "utopia-php/servers",
"version": "0.3.0",
"version": "0.4.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/servers.git",
"reference": "235be31200df9437fc96a1c270ffef4c64fafe52"
"reference": "7db346ef377503efe0acafe0791085270cd9ed70"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/servers/zipball/235be31200df9437fc96a1c270ffef4c64fafe52",
"reference": "235be31200df9437fc96a1c270ffef4c64fafe52",
"url": "https://api.github.com/repos/utopia-php/servers/zipball/7db346ef377503efe0acafe0791085270cd9ed70",
"reference": "7db346ef377503efe0acafe0791085270cd9ed70",
"shasum": ""
},
"require": {
@@ -4970,9 +4970,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/servers/issues",
"source": "https://github.com/utopia-php/servers/tree/0.3.0"
"source": "https://github.com/utopia-php/servers/tree/0.4.0"
},
"time": "2026-03-13T11:31:42+00:00"
"time": "2026-05-05T04:08:30+00:00"
},
{
"name": "utopia-php/span",
@@ -5020,16 +5020,16 @@
},
{
"name": "utopia-php/storage",
"version": "2.0.1",
"version": "2.0.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/storage.git",
"reference": "8a2e3a86fd01aaed675884146665308c2122264e"
"reference": "64e132a3768e22243eda36fe4262da22fd204f3c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/storage/zipball/8a2e3a86fd01aaed675884146665308c2122264e",
"reference": "8a2e3a86fd01aaed675884146665308c2122264e",
"url": "https://api.github.com/repos/utopia-php/storage/zipball/64e132a3768e22243eda36fe4262da22fd204f3c",
"reference": "64e132a3768e22243eda36fe4262da22fd204f3c",
"shasum": ""
},
"require": {
@@ -5066,22 +5066,22 @@
],
"support": {
"issues": "https://github.com/utopia-php/storage/issues",
"source": "https://github.com/utopia-php/storage/tree/2.0.1"
"source": "https://github.com/utopia-php/storage/tree/2.0.2"
},
"time": "2026-04-29T09:05:48+00:00"
"time": "2026-05-01T15:06:16+00:00"
},
{
"name": "utopia-php/system",
"version": "0.10.1",
"version": "0.10.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/system.git",
"reference": "7c1669533bb9c285de19191270c8c1439161a78a"
"reference": "04229a822b147c1abaf1a92fb42c2d7aad4625df"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/system/zipball/7c1669533bb9c285de19191270c8c1439161a78a",
"reference": "7c1669533bb9c285de19191270c8c1439161a78a",
"url": "https://api.github.com/repos/utopia-php/system/zipball/04229a822b147c1abaf1a92fb42c2d7aad4625df",
"reference": "04229a822b147c1abaf1a92fb42c2d7aad4625df",
"shasum": ""
},
"require": {
@@ -5122,9 +5122,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/system/issues",
"source": "https://github.com/utopia-php/system/tree/0.10.1"
"source": "https://github.com/utopia-php/system/tree/0.10.2"
},
"time": "2026-03-15T21:07:41+00:00"
"time": "2026-05-05T14:33:41+00:00"
},
{
"name": "utopia-php/telemetry",
@@ -5384,16 +5384,16 @@
},
{
"name": "webonyx/graphql-php",
"version": "v15.31.5",
"version": "v15.32.3",
"source": {
"type": "git",
"url": "https://github.com/webonyx/graphql-php.git",
"reference": "089c4ef7e112df85788cfe06596278a8f99f4aa9"
"reference": "993bf0bea17f870412ad8a90f60c41cb8d5f1145"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webonyx/graphql-php/zipball/089c4ef7e112df85788cfe06596278a8f99f4aa9",
"reference": "089c4ef7e112df85788cfe06596278a8f99f4aa9",
"url": "https://api.github.com/repos/webonyx/graphql-php/zipball/993bf0bea17f870412ad8a90f60c41cb8d5f1145",
"reference": "993bf0bea17f870412ad8a90f60c41cb8d5f1145",
"shasum": ""
},
"require": {
@@ -5402,16 +5402,16 @@
"php": "^7.4 || ^8"
},
"require-dev": {
"amphp/amp": "^2.6",
"amphp/http-server": "^2.1",
"amphp/amp": "^2.6 || ^3",
"amphp/http-server": "^2.1 || ^3",
"dms/phpunit-arraysubset-asserts": "dev-master",
"ergebnis/composer-normalize": "^2.28",
"friendsofphp/php-cs-fixer": "3.94.2",
"friendsofphp/php-cs-fixer": "3.95.1",
"mll-lab/php-cs-fixer-config": "5.13.0",
"nyholm/psr7": "^1.5",
"phpbench/phpbench": "^1.2",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "2.1.46",
"phpstan/phpstan": "2.1.51",
"phpstan/phpstan-phpunit": "2.0.16",
"phpstan/phpstan-strict-rules": "2.0.10",
"phpunit/phpunit": "^9.5 || ^10.5.21 || ^11",
@@ -5425,6 +5425,7 @@
"ticketswap/phpstan-error-formatter": "1.3.0"
},
"suggest": {
"amphp/amp": "To leverage async resolving on AMPHP platform (v3 with AmpFutureAdapter, v2 with AmpPromiseAdapter)",
"amphp/http-server": "To leverage async resolving with webserver on AMPHP platform",
"psr/http-message": "To use standard GraphQL server",
"react/promise": "To leverage async resolving on React PHP platform"
@@ -5447,7 +5448,7 @@
],
"support": {
"issues": "https://github.com/webonyx/graphql-php/issues",
"source": "https://github.com/webonyx/graphql-php/tree/v15.31.5"
"source": "https://github.com/webonyx/graphql-php/tree/v15.32.3"
},
"funding": [
{
@@ -5459,22 +5460,22 @@
"type": "open_collective"
}
],
"time": "2026-04-11T18:06:15+00:00"
"time": "2026-04-24T13:49:35+00:00"
}
],
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "1.25.1",
"version": "1.27.5",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "f21a556b9acdbf75bbdcdc90a078af641646eade"
"reference": "9faa38b48d422f3da764a719712905c83b3922cb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/f21a556b9acdbf75bbdcdc90a078af641646eade",
"reference": "f21a556b9acdbf75bbdcdc90a078af641646eade",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/9faa38b48d422f3da764a719712905c83b3922cb",
"reference": "9faa38b48d422f3da764a719712905c83b3922cb",
"shasum": ""
},
"require": {
@@ -5510,9 +5511,9 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": {
"issues": "https://github.com/appwrite/sdk-generator/issues",
"source": "https://github.com/appwrite/sdk-generator/tree/1.25.1"
"source": "https://github.com/appwrite/sdk-generator/tree/1.27.5"
},
"time": "2026-04-28T11:12:22+00:00"
"time": "2026-05-05T12:09:40+00:00"
},
{
"name": "brianium/paratest",
@@ -6619,16 +6620,16 @@
},
{
"name": "phpunit/phpunit",
"version": "12.5.23",
"version": "12.5.24",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969"
"reference": "d75dd30597caa80e72fad2ef7904601a30ef1046"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969",
"reference": "c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d75dd30597caa80e72fad2ef7904601a30ef1046",
"reference": "d75dd30597caa80e72fad2ef7904601a30ef1046",
"shasum": ""
},
"require": {
@@ -6697,7 +6698,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.23"
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.24"
},
"funding": [
{
@@ -6705,7 +6706,7 @@
"type": "other"
}
],
"time": "2026-04-18T06:12:49+00:00"
"time": "2026-05-01T04:21:04+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -7690,16 +7691,16 @@
},
{
"name": "symfony/console",
"version": "v8.0.8",
"version": "v8.0.9",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "5b66d385dc58f69652e56f78a4184615e3f2b7f7"
"reference": "7113778e2e91f4709cb3194a75dfa9c0d028d94d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/5b66d385dc58f69652e56f78a4184615e3f2b7f7",
"reference": "5b66d385dc58f69652e56f78a4184615e3f2b7f7",
"url": "https://api.github.com/repos/symfony/console/zipball/7113778e2e91f4709cb3194a75dfa9c0d028d94d",
"reference": "7113778e2e91f4709cb3194a75dfa9c0d028d94d",
"shasum": ""
},
"require": {
@@ -7756,7 +7757,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v8.0.8"
"source": "https://github.com/symfony/console/tree/v8.0.9"
},
"funding": [
{
@@ -7776,7 +7777,7 @@
"type": "tidelift"
}
],
"time": "2026-03-30T15:14:47+00:00"
"time": "2026-04-29T15:02:55+00:00"
},
{
"name": "symfony/polyfill-ctype",
@@ -8444,7 +8445,7 @@
],
"aliases": [],
"minimum-stability": "dev",
"stability-flags": [],
"stability-flags": {},
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
@@ -8465,5 +8466,5 @@
"platform-dev": {
"ext-fileinfo": "*"
},
"plugin-api-version": "2.6.0"
"plugin-api-version": "2.9.0"
}
@@ -0,0 +1 @@
Create a bigint attribute. Optionally, minimum and maximum values can be provided.
@@ -0,0 +1 @@
Update a bigint attribute. Changing the `default` value will not update already existing documents.
@@ -0,0 +1 @@
Create a bigint column. Optionally, minimum and maximum values can be provided.
@@ -0,0 +1 @@
Update a bigint column. Changing the `default` value will not update already existing rows.
+2
View File
@@ -178,6 +178,7 @@ class Exception extends \Exception
public const string FUNCTION_RUNTIME_UNSUPPORTED = 'function_runtime_unsupported';
public const string FUNCTION_ENTRYPOINT_MISSING = 'function_entrypoint_missing';
public const string FUNCTION_SYNCHRONOUS_TIMEOUT = 'function_synchronous_timeout';
public const string FUNCTION_ASYNCHRONOUS_TIMEOUT = 'function_asynchronous_timeout';
public const string FUNCTION_TEMPLATE_NOT_FOUND = 'function_template_not_found';
public const string FUNCTION_RUNTIME_NOT_DETECTED = 'function_runtime_not_detected';
public const string FUNCTION_EXECUTE_PERMISSION_MISSING = 'function_execute_permission_missing';
@@ -192,6 +193,7 @@ class Exception extends \Exception
public const string BUILD_ALREADY_COMPLETED = 'build_already_completed';
public const string BUILD_CANCELED = 'build_canceled';
public const string BUILD_FAILED = 'build_failed';
public const string BUILD_TIMEOUT = 'build_timeout';
/** Execution */
public const string EXECUTION_NOT_FOUND = 'execution_not_found';
+1
View File
@@ -96,6 +96,7 @@ abstract class Migration
'1.9.1' => 'V24',
'1.9.2' => 'V24',
'1.9.3' => 'V24',
'1.9.4' => 'V24',
];
/**
@@ -241,6 +241,10 @@ abstract class Action extends UtopiaAction
? UtopiaResponse::MODEL_ATTRIBUTE_INTEGER
: UtopiaResponse::MODEL_COLUMN_INTEGER,
Database::VAR_BIGINT => $isCollections
? UtopiaResponse::MODEL_ATTRIBUTE_BIGINT
: UtopiaResponse::MODEL_COLUMN_BIGINT,
Database::VAR_FLOAT => $isCollections
? UtopiaResponse::MODEL_ATTRIBUTE_FLOAT
: UtopiaResponse::MODEL_COLUMN_FLOAT,
@@ -540,6 +544,7 @@ abstract class Action extends UtopiaAction
switch ($attribute->getAttribute('format')) {
case APP_DATABASE_ATTRIBUTE_INT_RANGE:
case APP_DATABASE_ATTRIBUTE_BIGINT_RANGE:
case APP_DATABASE_ATTRIBUTE_FLOAT_RANGE:
$min ??= $attribute->getAttribute('formatOptions')['min'];
$max ??= $attribute->getAttribute('formatOptions')['max'];
@@ -548,14 +553,15 @@ abstract class Action extends UtopiaAction
throw new Exception($this->getInvalidValueException(), 'Minimum value must be lesser than maximum value');
}
if ($attribute->getAttribute('format') === APP_DATABASE_ATTRIBUTE_INT_RANGE) {
$validator = new Range($min, $max, Database::VAR_INTEGER);
} else {
if ($attribute->getAttribute('format') === APP_DATABASE_ATTRIBUTE_FLOAT_RANGE) {
$validator = new Range($min, $max, Database::VAR_FLOAT);
if (!is_null($default)) {
$default = \floatval($default);
}
} else {
// intRange and bigintRange share the same integer range semantics
$validator = new Range($min, $max, Range::TYPE_INTEGER);
}
if (!is_null($default) && !$validator->isValid($default)) {
@@ -0,0 +1,117 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\BigInt;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Deprecated;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Integer;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
class Create extends Action
{
public static function getName(): string
{
return 'createBigIntAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_BIGINT;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/bigint')
->desc('Create bigint attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].create')
->label('audits.event', 'attribute.create')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSDKNamespace(),
group: $this->getSDKGroup(),
name: self::getName(),
description: '/docs/references/databases/create-bigint-attribute.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel(),
)
],
deprecated: new Deprecated(
since: '1.8.0',
replaceWith: 'tablesDB.createBigIntColumn',
),
))
->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
->param('collectionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Collection ID.', false, ['dbForProject'])
->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Attribute Key.', false, ['dbForProject'])
->param('required', null, new Boolean(), 'Is attribute required?')
->param('min', null, new Nullable(new Integer(false, 64)), 'Minimum value', true)
->param('max', null, new Nullable(new Integer(false, 64)), 'Maximum value', true)
->param('default', null, new Nullable(new Integer(false, 64)), 'Default value. Cannot be set when attribute is required.', true)
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$min ??= \PHP_INT_MIN;
$max ??= \PHP_INT_MAX;
if ($min > $max) {
throw new Exception($this->getInvalidValueException(), 'Minimum value must be lesser than maximum value');
}
$validator = new Range($min, $max, Range::TYPE_INTEGER);
if (!\is_null($default) && !$validator->isValid($default)) {
throw new Exception($this->getInvalidValueException(), $validator->getDescription());
}
$attribute = $this->createAttribute($databaseId, $collectionId, new Document([
'key' => $key,
'type' => Database::VAR_BIGINT,
'size' => 8,
'required' => $required,
'default' => $default,
'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_BIGINT_RANGE,
'formatOptions' => ['min' => $min, 'max' => $max],
]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
$formatOptions = $attribute->getAttribute('formatOptions', []);
if (!empty($formatOptions)) {
$attribute->setAttribute('min', \intval($formatOptions['min']));
$attribute->setAttribute('max', \intval($formatOptions['max']));
}
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
->dynamic($attribute, $this->getResponseModel());
}
}
@@ -0,0 +1,106 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\BigInt;
use Appwrite\Event\Event;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Deprecated;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Integer;
use Utopia\Validator\Nullable;
class Update extends Action
{
public static function getName(): string
{
return 'updateBigIntAttribute';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_ATTRIBUTE_BIGINT;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/attributes/bigint/:key')
->desc('Update bigint attribute')
->groups(['api', 'database', 'schema'])
->label('scope', 'collections.write')
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].collections.[collectionId].attributes.[attributeId].update')
->label('audits.event', 'attribute.update')
->label('audits.resource', 'database/{request.databaseId}/collection/{request.collectionId}')
->label('sdk', new Method(
namespace: $this->getSDKNamespace(),
group: $this->getSDKGroup(),
name: self::getName(),
description: '/docs/references/databases/update-bigint-attribute.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON,
deprecated: new Deprecated(
since: '1.8.0',
replaceWith: 'tablesDB.updateBigIntColumn',
),
))
->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
->param('collectionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Collection ID.', false, ['dbForProject'])
->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Attribute Key.', false, ['dbForProject'])
->param('required', null, new Boolean(), 'Is attribute required?')
->param('min', null, new Nullable(new Integer(false, 64)), 'Minimum value', true)
->param('max', null, new Nullable(new Integer(false, 64)), 'Maximum value', true)
->param('default', null, new Nullable(new Integer(false, 64)), 'Default value. Cannot be set when attribute is required.')
->param('newKey', null, fn (Database $dbForProject) => new Nullable(new Key(false, $dbForProject->getAdapter()->getMaxUIDLength())), 'New Attribute Key.', true, ['dbForProject'])
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, ?string $newKey, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents, Authorization $authorization): void
{
$attribute = $this->updateAttribute(
databaseId: $databaseId,
collectionId: $collectionId,
key: $key,
dbForProject: $dbForProject,
queueForEvents: $queueForEvents,
authorization: $authorization,
type: Database::VAR_BIGINT,
default: $default,
required: $required,
min: $min,
max: $max,
newKey: $newKey
);
$formatOptions = $attribute->getAttribute('formatOptions', []);
if (!empty($formatOptions)) {
$attribute->setAttribute('min', \intval($formatOptions['min']));
$attribute->setAttribute('max', \intval($formatOptions['max']));
}
$response
->setStatusCode(SwooleResponse::STATUS_CODE_OK)
->dynamic($attribute, $this->getResponseModel());
}
}
@@ -290,13 +290,15 @@ class Create extends Action
}
if (isset($attribute['min']) || isset($attribute['max'])) {
$format = $type === Database::VAR_INTEGER
? APP_DATABASE_ATTRIBUTE_INT_RANGE
: APP_DATABASE_ATTRIBUTE_FLOAT_RANGE;
$format = match($type) {
Database::VAR_INTEGER => APP_DATABASE_ATTRIBUTE_INT_RANGE,
Database::VAR_BIGINT => APP_DATABASE_ATTRIBUTE_BIGINT_RANGE,
default => APP_DATABASE_ATTRIBUTE_FLOAT_RANGE,
};
$formatOptions = [
'min' => $attribute['min'] ?? ($type === Database::VAR_INTEGER ? \PHP_INT_MIN : -\PHP_FLOAT_MAX),
'max' => $attribute['max'] ?? ($type === Database::VAR_INTEGER ? \PHP_INT_MAX : \PHP_FLOAT_MAX),
'min' => $attribute['min'] ?? ($type === Database::VAR_INTEGER || $type === Database::VAR_BIGINT ? \PHP_INT_MIN : -\PHP_FLOAT_MAX),
'max' => $attribute['max'] ?? ($type === Database::VAR_INTEGER || $type === Database::VAR_BIGINT ? \PHP_INT_MAX : \PHP_FLOAT_MAX),
];
}
@@ -0,0 +1,70 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\BigInt;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\BigInt\Create as BigIntCreate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Integer;
use Utopia\Validator\Nullable;
class Create extends BigIntCreate
{
public static function getName(): string
{
return 'createBigIntColumn';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_COLUMN_BIGINT;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/bigint')
->desc('Create bigint column')
->groups(['api', 'database', 'schema'])
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].create')
->label('audits.event', 'column.create')
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
->label('sdk', new Method(
namespace: $this->getSDKNamespace(),
group: $this->getSDKGroup(),
name: self::getName(),
description: '/docs/references/tablesdb/create-bigint-column.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_ACCEPTED,
model: $this->getResponseModel(),
)
]
))
->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
->param('tableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Table ID.', false, ['dbForProject'])
->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Column Key.', false, ['dbForProject'])
->param('required', null, new Boolean(), 'Is column required?')
->param('min', null, new Nullable(new Integer(false, 64)), 'Minimum value', true)
->param('max', null, new Nullable(new Integer(false, 64)), 'Maximum value', true)
->param('default', null, new Nullable(new Integer(false, 64)), 'Default value. Cannot be set when column is required.', true)
->param('array', false, new Boolean(), 'Is column an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
}
@@ -0,0 +1,71 @@
<?php
namespace Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\BigInt;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\BigInt\Update as BigIntUpdate;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
use Utopia\Validator\Integer;
use Utopia\Validator\Nullable;
class Update extends BigIntUpdate
{
public static function getName(): string
{
return 'updateBigIntColumn';
}
protected function getResponseModel(): string|array
{
return UtopiaResponse::MODEL_COLUMN_BIGINT;
}
public function __construct()
{
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/columns/bigint/:key')
->desc('Update bigint column')
->groups(['api', 'database', 'schema'])
->label('scope', ['tables.write', 'collections.write', 'columns.write', 'attributes.write'])
->label('resourceType', RESOURCE_TYPE_DATABASES)
->label('event', 'databases.[databaseId].tables.[tableId].columns.[columnId].update')
->label('audits.event', 'column.update')
->label('audits.resource', 'database/{request.databaseId}/table/{request.tableId}')
->label('sdk', new Method(
namespace: $this->getSDKNamespace(),
group: $this->getSDKGroup(),
name: self::getName(),
description: '/docs/references/tablesdb/update-bigint-column.md',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: SwooleResponse::STATUS_CODE_OK,
model: $this->getResponseModel(),
)
],
contentType: ContentType::JSON
))
->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
->param('tableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Table ID.', false, ['dbForProject'])
->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Column Key.', false, ['dbForProject'])
->param('required', null, new Boolean(), 'Is column required?')
->param('min', null, new Nullable(new Integer(false, 64)), 'Minimum value', true)
->param('max', null, new Nullable(new Integer(false, 64)), 'Maximum value', true)
->param('default', null, new Nullable(new Integer(false, 64)), 'Default value. Cannot be set when column is required.')
->param('newKey', null, fn (Database $dbForProject) => new Nullable(new Key(false, $dbForProject->getAdapter()->getMaxUIDLength())), 'New Column Key.', true, ['dbForProject'])
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
}
@@ -2,6 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Services\Registry;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\BigInt\Create as CreateBigIntAttribute;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\BigInt\Update as UpdateBigIntAttribute;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Boolean\Create as CreateBooleanAttribute;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Boolean\Update as UpdateBooleanAttribute;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Datetime\Create as CreateDatetimeAttribute;
@@ -171,6 +173,10 @@ class Legacy extends Base
$service->addAction(CreateIntegerAttribute::getName(), new CreateIntegerAttribute());
$service->addAction(UpdateIntegerAttribute::getName(), new UpdateIntegerAttribute());
// Attribute: BigInt
$service->addAction(CreateBigIntAttribute::getName(), new CreateBigIntAttribute());
$service->addAction(UpdateBigIntAttribute::getName(), new UpdateBigIntAttribute());
// Attribute: IP
$service->addAction(CreateIPAttribute::getName(), new CreateIPAttribute());
$service->addAction(UpdateIPAttribute::getName(), new UpdateIPAttribute());
@@ -5,6 +5,8 @@ namespace Appwrite\Platform\Modules\Databases\Services\Registry;
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Create as CreateTablesDatabase;
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Delete as DeleteTablesDatabase;
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Get as GetTablesDatabase;
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\BigInt\Create as CreateBigInt;
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\BigInt\Update as UpdateBigInt;
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Boolean\Create as CreateBoolean;
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Boolean\Update as UpdateBoolean;
use Appwrite\Platform\Modules\Databases\Http\TablesDB\Tables\Columns\Datetime\Create as CreateDatetime;
@@ -151,6 +153,10 @@ class TablesDB extends Base
$service->addAction(CreateInteger::getName(), new CreateInteger());
$service->addAction(UpdateInteger::getName(), new UpdateInteger());
// Column: BigInt
$service->addAction(CreateBigInt::getName(), new CreateBigInt());
$service->addAction(UpdateBigInt::getName(), new UpdateBigInt());
// Column: IP
$service->addAction(CreateIP::getName(), new CreateIP());
$service->addAction(UpdateIP::getName(), new UpdateIP());
@@ -17,6 +17,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Response;
use Executor\Exception\Timeout as ExecutorTimeout;
use Executor\Executor;
use MaxMind\Db\Reader;
use Utopia\Auth\Proofs\Token;
@@ -417,25 +418,29 @@ class Create extends Base
$source = $deployment->getAttribute('buildPath', '');
$extension = str_ends_with($source, '.tar') ? 'tar' : 'tar.gz';
$command = $version === 'v2' ? '' : "cp /tmp/code.$extension /mnt/code/code.$extension && nohup helpers/start.sh \"$command\"";
$executionResponse = $executor->createExecution(
projectId: $project->getId(),
deploymentId: $deployment->getId(),
body: \strlen($body) > 0 ? $body : null,
variables: $vars,
timeout: $function->getAttribute('timeout', 0),
image: $runtime['image'],
source: $source,
entrypoint: $deployment->getAttribute('entrypoint', ''),
version: $version,
path: $path,
method: $method,
headers: $headers,
runtimeEntrypoint: $command,
cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT,
memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT,
logging: $function->getAttribute('logging', true),
requestTimeout: 30
);
try {
$executionResponse = $executor->createExecution(
projectId: $project->getId(),
deploymentId: $deployment->getId(),
body: \strlen($body) > 0 ? $body : null,
variables: $vars,
timeout: $function->getAttribute('timeout', 0),
image: $runtime['image'],
source: $source,
entrypoint: $deployment->getAttribute('entrypoint', ''),
version: $version,
path: $path,
method: $method,
headers: $headers,
runtimeEntrypoint: $command,
cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT,
memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT,
logging: $function->getAttribute('logging', true),
requestTimeout: 30
);
} catch (ExecutorTimeout $th) {
throw new AppwriteException(AppwriteException::FUNCTION_SYNCHRONOUS_TIMEOUT, previous: $th);
}
$headersFiltered = [];
foreach ($executionResponse['headers'] as $key => $value) {
@@ -2,11 +2,13 @@
namespace Appwrite\Platform\Modules\Functions\Http\Variables;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
@@ -38,6 +40,7 @@ class Create extends Base
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('event', 'variables.[variableId].create')
->label('audits.event', 'variable.create')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
@@ -56,10 +59,12 @@ class Create extends Base
]
))
->param('functionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Function unique ID.', false, ['dbForProject'])
->param('variableId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject'])
->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false)
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false)
->param('secret', true, new Boolean(), 'Secret variables can be updated or deleted, but only functions can read them during build and runtime.', true)
->inject('response')
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('project')
@@ -69,10 +74,12 @@ class Create extends Base
public function action(
string $functionId,
string $variableId,
string $key,
string $value,
bool $secret,
Response $response,
QueueEvent $queueForEvents,
Database $dbForProject,
Database $dbForPlatform,
Document $project,
@@ -84,7 +91,7 @@ class Create extends Base
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$variableId = ID::unique();
$variableId = ($variableId === 'unique()') ? ID::unique() : $variableId;
$teamId = $project->getAttribute('teamId', '');
$variable = new Document([
@@ -120,6 +127,8 @@ class Create extends Base
'active' => $schedule->getAttribute('active'),
])));
$queueForEvents->setParam('variableId', $variable->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($variable, Response::MODEL_VARIABLE);
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Modules\Functions\Http\Variables;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
@@ -35,6 +36,7 @@ class Delete extends Base
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('event', 'variables.[variableId].delete')
->label('audits.event', 'variable.delete')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
@@ -56,6 +58,7 @@ class Delete extends Base
->param('functionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Function unique ID.', false, ['dbForProject'])
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
->inject('response')
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('authorization')
@@ -66,6 +69,7 @@ class Delete extends Base
string $functionId,
string $variableId,
Response $response,
QueueEvent $queueForEvents,
Database $dbForProject,
Database $dbForPlatform,
Authorization $authorization
@@ -98,6 +102,8 @@ class Delete extends Base
'active' => $schedule->getAttribute('active'),
])));
$queueForEvents->setParam('variableId', $variable->getId());
$response->noContent();
}
}
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Modules\Functions\Http\Variables;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
@@ -38,6 +39,7 @@ class Update extends Base
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('event', 'variables.[variableId].update')
->label('audits.event', 'variable.update')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
@@ -57,10 +59,11 @@ class Update extends Base
))
->param('functionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Function unique ID.', false, ['dbForProject'])
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
->param('key', null, new Text(255), 'Variable key. Max length: 255 chars.', false)
->param('key', null, new Nullable(new Text(255, 0)), 'Variable key. Max length: 255 chars.', true)
->param('value', null, new Nullable(new Text(8192, 0)), 'Variable value. Max length: 8192 chars.', true)
->param('secret', null, new Nullable(new Boolean()), 'Secret variables can be updated or deleted, but only functions can read them during build and runtime.', true)
->inject('response')
->inject('queueForEvents')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('authorization')
@@ -70,10 +73,11 @@ class Update extends Base
public function action(
string $functionId,
string $variableId,
string $key,
?string $key,
?string $value,
?bool $secret,
Response $response,
QueueEvent $queueForEvents,
Database $dbForProject,
Database $dbForPlatform,
Authorization $authorization
@@ -93,19 +97,27 @@ class Update extends Base
throw new Exception(Exception::VARIABLE_CANNOT_UNSET_SECRET);
}
$variable
->setAttribute('key', $key)
->setAttribute('value', $value ?? $variable->getAttribute('value'))
->setAttribute('secret', $secret ?? $variable->getAttribute('secret'))
->setAttribute('search', implode(' ', [$variableId, $function->getId(), $key, 'function']));
if (\is_null($key) && \is_null($value) && \is_null($secret)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID);
}
$updates = new Document();
if (!\is_null($key)) {
$updates->setAttribute('key', $key);
$updates->setAttribute('search', implode(' ', [$variableId, $function->getId(), $key, 'function']));
}
if (!\is_null($value)) {
$updates->setAttribute('value', $value);
}
if (!\is_null($secret)) {
$updates->setAttribute('secret', $secret);
}
try {
$dbForProject->updateDocument('variables', $variable->getId(), new Document([
'key' => $key,
'value' => $value ?? $variable->getAttribute('value'),
'secret' => $secret ?? $variable->getAttribute('secret'),
'search' => implode(' ', [$variableId, $function->getId(), $key, 'function']),
]));
$variable = $dbForProject->updateDocument('variables', $variable->getId(), $updates);
} catch (DuplicateException $th) {
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
}
@@ -125,6 +137,8 @@ class Update extends Base
'active' => $schedule->getAttribute('active'),
])));
$queueForEvents->setParam('variableId', $variable->getId());
$response->dynamic($variable, Response::MODEL_VARIABLE);
}
}
@@ -7,12 +7,18 @@ use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Variables;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Order as OrderException;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
class XList extends Base
{
@@ -51,22 +57,74 @@ class XList extends Base
)
)
->param('functionId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Function unique ID.', false, ['dbForProject'])
->param('queries', [], new Variables(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Variables::ALLOWED_ATTRIBUTES), true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForProject')
->callback($this->action(...));
}
public function action(string $functionId, Response $response, Database $dbForProject)
{
/**
* @param array<string> $queries
*/
public function action(
string $functionId,
array $queries,
bool $includeTotal,
Response $response,
Database $dbForProject
) {
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$queries[] = Query::equal('resourceType', ['function']);
$queries[] = Query::equal('resourceInternalId', [$function->getSequence()]);
$queries[] = Query::orderAsc();
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$variableId = $cursor->getValue();
$cursorDocument = $dbForProject->findOne('variables', [
Query::equal('$id', [$variableId]),
Query::equal('resourceType', ['function']),
Query::equal('resourceInternalId', [$function->getSequence()]),
]);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Variable '{$variableId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
try {
$variables = $dbForProject->find('variables', $queries);
$total = $includeTotal ? $dbForProject->count('variables', $filterQueries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
$response->dynamic(new Document([
'variables' => $function->getAttribute('vars', []),
'total' => \count($function->getAttribute('vars', [])),
'variables' => $variables,
'total' => $total,
]), Response::MODEL_VARIABLE_LIST);
}
}
@@ -10,11 +10,13 @@ use Appwrite\Event\Publisher\Screenshot;
use Appwrite\Event\Publisher\Usage as UsagePublisher;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Filter\BranchDomain as BranchDomainFilter;
use Appwrite\Usage\Context;
use Appwrite\Utopia\Response\Model\Deployment;
use Appwrite\Vcs\Comment;
use Exception;
use Executor\Exception\Timeout as ExecutorTimeout;
use Executor\Executor;
use Swoole\Coroutine as Co;
use Utopia\Cache\Cache;
@@ -34,6 +36,7 @@ use Utopia\Detector\Detector\Rendering;
use Utopia\Logger\Log;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\Span\Span;
use Utopia\Storage\Device;
use Utopia\Storage\Device\Local;
use Utopia\System\System;
@@ -183,6 +186,12 @@ class Builds extends Action
array $platform,
int $timeout
): void {
Span::add('projectId', $project->getId());
Span::add('resourceId', $resource->getId());
Span::add('resourceType', $resource->getCollection());
Span::add('deploymentId', $deployment->getId());
Span::add('timeout', $timeout);
Console::info('Deployment action started');
$startTime = DateTime::now();
@@ -223,8 +232,12 @@ class Builds extends Action
$version = $this->getVersion($resource);
$runtime = $this->getRuntime($resource, $version);
Span::add('runtime', $resource->getAttribute($resource->getCollection() === 'sites' ? 'buildRuntime' : 'runtime', ''));
Span::add('version', $version);
$spec = Config::getParam('specifications')[$resource->getAttribute('buildSpecification', APP_COMPUTE_SPECIFICATION_DEFAULT)];
Span::add('cpus', (float) ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT));
Span::add('memory', (int) ($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT));
// Realtime preparation
$event = "{$resource->getCollection()}.[{$resourceKey}].deployments.[deploymentId].update";
@@ -720,6 +733,9 @@ class Builds extends Action
);
Console::log('createRuntime finished');
} catch (ExecutorTimeout $error) {
Console::warning('createRuntime timed out');
$err = new AppwriteException(AppwriteException::BUILD_TIMEOUT, previous: $error);
} catch (\Throwable $error) {
Console::warning('createRuntime failed');
$err = $error;
@@ -1147,13 +1163,11 @@ class Builds extends Action
$message = \str_replace('{APPWRITE_DETECTION_SEPARATOR_START}', '', $message);
$message = \str_replace('{APPWRITE_DETECTION_SEPARATOR_END}', '', $message);
// Combine with previous logs if deployment got past build process
$previousLogs = '';
if (! is_null($deployment->getAttribute('buildSize', null))) {
$previousLogs = $deployment->getAttribute('buildLogs', '');
if (! empty($previousLogs)) {
$message = $previousLogs . "\n" . $message;
}
// Append error to whatever build logs were already streamed
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
$previousLogs = $deployment->getAttribute('buildLogs', '');
if (! empty($previousLogs)) {
$message = $previousLogs . "\n" . $message;
}
$endTime = DateTime::now();
@@ -13,6 +13,7 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\UID;
use Utopia\Migration\Destinations\OnDuplicate;
use Utopia\Migration\Sources\Appwrite as AppwriteSource;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
@@ -57,6 +58,7 @@ class Create extends Action
->param('endpoint', '', new URL(), 'Source Appwrite endpoint')
->param('projectId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Source Project ID', false, ['dbForProject'])
->param('apiKey', '', new Text(512), 'Source API Key')
->param('onDuplicate', OnDuplicate::Fail->value, new WhiteList(OnDuplicate::values()), 'Behavior when a row with an existing $id is encountered. "fail" (default): abort on first conflict. "skip": silently ignore. "overwrite": replace existing row.', true)
->inject('response')
->inject('dbForProject')
->inject('project')
@@ -71,6 +73,7 @@ class Create extends Action
string $endpoint,
string $projectId,
string $apiKey,
string $onDuplicate,
Response $response,
Database $dbForProject,
Document $project,
@@ -93,6 +96,9 @@ class Create extends Action
'statusCounters' => '{}',
'resourceData' => '{}',
'errors' => [],
'options' => [
'onDuplicate' => $onDuplicate,
],
]));
$queueForEvents->setParam('migrationId', $migration->getId());
@@ -20,6 +20,7 @@ use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Migration\Destinations\OnDuplicate;
use Utopia\Migration\Resource;
use Utopia\Migration\Sources\Appwrite as AppwriteSource;
use Utopia\Migration\Sources\CSV;
@@ -29,6 +30,7 @@ use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
use Utopia\System\System;
use Utopia\Validator\Boolean;
use Utopia\Validator\WhiteList;
class Create extends Action
{
@@ -67,6 +69,7 @@ class Create extends Action
->param('fileId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'File ID.', false, ['dbForProject'])
->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database.')
->param('internalFile', false, new Boolean(), 'Is the file stored in an internal bucket?', true)
->param('onDuplicate', OnDuplicate::Fail->value, new WhiteList(OnDuplicate::values()), 'Behavior when a row with an existing $id is encountered. "fail" (default): abort on first conflict. "skip": silently ignore. "overwrite": replace existing row.', true)
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
@@ -85,6 +88,7 @@ class Create extends Action
string $fileId,
string $resourceId,
bool $internalFile,
string $onDuplicate,
Response $response,
Database $dbForProject,
Database $dbForPlatform,
@@ -183,6 +187,7 @@ class Create extends Action
'options' => [
'path' => $newPath,
'size' => $fileSize,
'onDuplicate' => $onDuplicate,
],
]));
@@ -20,6 +20,7 @@ use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Migration\Destinations\OnDuplicate;
use Utopia\Migration\Resource;
use Utopia\Migration\Sources\Appwrite as AppwriteSource;
use Utopia\Migration\Sources\JSON as JSONSource;
@@ -29,6 +30,7 @@ use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
use Utopia\System\System;
use Utopia\Validator\Boolean;
use Utopia\Validator\WhiteList;
class Create extends Action
{
@@ -66,6 +68,7 @@ class Create extends Action
->param('fileId', '', new UID(), 'File ID.')
->param('resourceId', null, new CompoundUID(), 'Composite ID in the format {databaseId:collectionId}, identifying a collection within a database.')
->param('internalFile', false, new Boolean(), 'Is the file stored in an internal bucket?', true)
->param('onDuplicate', OnDuplicate::Fail->value, new WhiteList(OnDuplicate::values()), 'Behavior when a row with an existing $id is encountered. "fail" (default): abort on first conflict. "skip": silently ignore. "overwrite": replace existing row.', true)
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
@@ -84,6 +87,7 @@ class Create extends Action
string $fileId,
string $resourceId,
bool $internalFile,
string $onDuplicate,
Response $response,
Database $dbForProject,
Database $dbForPlatform,
@@ -183,6 +187,7 @@ class Create extends Action
'options' => [
'path' => $newPath,
'size' => $fileSize,
'onDuplicate' => $onDuplicate,
],
]));
@@ -59,7 +59,7 @@ class Create extends Base
],
))
->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.', optional: false)
->param('duration', null, new Range(1, 3600), 'Time in seconds before ephemeral key expires. Maximum duration is 3600 seconds.', optional: false)
->param('duration', null, new Range(1, 3600), 'Time in seconds before ephemeral key expires. Maximum duration is 3600 seconds.', optional: false, example: 600)
->inject('response')
->inject('queueForEvents')
->inject('project')
@@ -11,7 +11,7 @@ use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
class Get extends Action
{
@@ -86,28 +86,28 @@ class Get extends Action
)
]
))
->param('provider', '', new Text(128), 'OAuth2 provider key. For example: github, google, apple.')
->param('providerId', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders', [])), true), 'OAuth2 provider key. For example: github, google, apple.', aliases: ['provider'])
->inject('response')
->inject('project')
->callback($this->action(...));
}
public function action(
string $provider,
string $providerId,
Response $response,
Document $project,
): void {
$providers = Config::getParam('oAuthProviders', []);
if (!\array_key_exists($provider, $providers) || !($providers[$provider]['enabled'] ?? false)) {
if (!\array_key_exists($providerId, $providers) || !($providers[$providerId]['enabled'] ?? false)) {
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
}
$actions = Base::getProviderActions();
if (!isset($actions[$provider])) {
if (!isset($actions[$providerId])) {
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
}
$updateClass = $actions[$provider];
$updateClass = $actions[$providerId];
$action = new $updateClass();
$response->dynamic($action->buildReadResponse($project), $updateClass::getResponseModel());
@@ -82,13 +82,13 @@ class Update extends Base
'hint' => '',
],
[
'$id' => 'tokenUrl',
'$id' => 'tokenURL',
'name' => 'Token URL',
'example' => 'https://myoauth.com/oauth2/token',
'hint' => '',
],
[
'$id' => 'userInfoUrl',
'$id' => 'userInfoURL',
'name' => 'User Info URL',
'example' => 'https://myoauth.com/oauth2/userinfo',
'hint' => '',
@@ -127,8 +127,8 @@ class Update extends Base
->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true)
->param('wellKnownURL', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect well-known configuration URL. When provided, authorization, token, and user info endpoints can be discovered automatically. For example: https://myoauth.com/.well-known/openid-configuration', optional: true)
->param('authorizationURL', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect authorization endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/authorize', optional: true)
->param('tokenUrl', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect token endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/token', optional: true)
->param('userInfoUrl', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect user info endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/userinfo', optional: true)
->param('tokenURL', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect token endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/token', optional: true, aliases: ['tokenUrl'])
->param('userInfoURL', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect user info endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/userinfo', optional: true, aliases: ['userInfoUrl'])
->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true)
->inject('response')
->inject('dbForPlatform')
@@ -151,8 +151,8 @@ class Update extends Base
static::getClientSecretParamName() => '',
'wellKnownURL' => $decoded['wellKnownEndpoint'] ?? '',
'authorizationURL' => $decoded['authorizationEndpoint'] ?? '',
'tokenUrl' => $decoded['tokenEndpoint'] ?? '',
'userInfoUrl' => $decoded['userInfoEndpoint'] ?? '',
'tokenURL' => $decoded['tokenEndpoint'] ?? '',
'userInfoURL' => $decoded['userInfoEndpoint'] ?? '',
]);
}
@@ -174,8 +174,8 @@ class Update extends Base
?string $clientSecret,
?string $wellKnownURL,
?string $authorizationURL,
?string $tokenUrl,
?string $userInfoUrl,
?string $tokenURL,
?string $userInfoURL,
?bool $enabled,
Response $response,
Database $dbForPlatform,
@@ -201,8 +201,8 @@ class Update extends Base
'clientSecret' => $clientSecret ?? ($existing['clientSecret'] ?? ''),
'wellKnownEndpoint' => $wellKnownURL ?? ($existing['wellKnownEndpoint'] ?? ''),
'authorizationEndpoint' => $authorizationURL ?? ($existing['authorizationEndpoint'] ?? ''),
'tokenEndpoint' => $tokenUrl ?? ($existing['tokenEndpoint'] ?? ''),
'userInfoEndpoint' => $userInfoUrl ?? ($existing['userInfoEndpoint'] ?? ''),
'tokenEndpoint' => $tokenURL ?? ($existing['tokenEndpoint'] ?? ''),
'userInfoEndpoint' => $userInfoURL ?? ($existing['userInfoEndpoint'] ?? ''),
];
// When enabling, require either wellKnownEndpoint alone, or all three
@@ -215,7 +215,7 @@ class Update extends Base
&& !empty($merged['userInfoEndpoint']);
if (!$hasWellKnown && !$hasAllDiscovery) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Enabling OpenID Connect requires either wellKnownURL, or all of authorizationURL, tokenUrl, and userInfoUrl.');
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Enabling OpenID Connect requires either wellKnownURL, or all of authorizationURL, tokenURL, and userInfoURL.');
}
}
@@ -2,14 +2,21 @@
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
class XList extends Action
{
@@ -43,15 +50,28 @@ class XList extends Action
)
]
))
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('project')
->callback($this->action(...));
}
/**
* @param array<string> $queries
*/
public function action(
array $queries,
bool $includeTotal,
Response $response,
Document $project,
): void {
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$providers = Config::getParam('oAuthProviders', []);
$actions = Base::getProviderActions();
@@ -66,8 +86,16 @@ class XList extends Action
$documents[] = $action->buildReadResponse($project);
}
$total = $includeTotal ? \count($documents) : 0;
$grouped = Query::groupByType($queries);
$offset = $grouped['offset'] ?? 0;
$limit = $grouped['limit'] ?? null;
$documents = \array_slice($documents, $offset, $limit);
$response->dynamic(new Document([
'total' => \count($documents),
'total' => $total,
'providers' => $documents,
]), Response::MODEL_OAUTH2_PROVIDER_LIST);
}
@@ -27,7 +27,7 @@ class Get extends Action
->setHttpPath('/v1/project/policies/:policyId')
->desc('Get project policy')
->groups(['api', 'project'])
->label('scope', 'policies.read')
->label('scope', ['policies.read', 'project.policies.read'])
->label('sdk', new Method(
namespace: 'project',
group: 'policies',
@@ -31,7 +31,7 @@ class Update extends Action
->httpAlias('/v1/projects/:projectId/auth/memberships-privacy')
->desc('Update membership privacy policy')
->groups(['api', 'project'])
->label('scope', 'policies.write')
->label('scope', ['policies.write', 'project.policies.write'])
->label('event', 'projects.[projectId].policies.[policy].update')
->label('audits.event', 'projects.[projectId].policies.[policy].update')
->label('audits.resource', 'project/{response.$id}')
@@ -31,7 +31,7 @@ class Update extends Action
->httpAlias('/v1/projects/:projectId/auth/password-dictionary')
->desc('Update password dictionary policy')
->groups(['api', 'project'])
->label('scope', 'policies.write')
->label('scope', ['policies.write', 'project.policies.write'])
->label('event', 'projects.[projectId].policies.[policy].update')
->label('audits.event', 'projects.[projectId].policies.[policy].update')
->label('audits.resource', 'project/{response.$id}')
@@ -32,7 +32,7 @@ class Update extends Action
->httpAlias('/v1/projects/:projectId/auth/password-history')
->desc('Update password history policy')
->groups(['api', 'project'])
->label('scope', 'policies.write')
->label('scope', ['policies.write', 'project.policies.write'])
->label('event', 'projects.[projectId].policies.[policy].update')
->label('audits.event', 'projects.[projectId].policies.[policy].update')
->label('audits.resource', 'project/{response.$id}')
@@ -31,7 +31,7 @@ class Update extends Action
->httpAlias('/v1/projects/:projectId/auth/personal-data')
->desc('Update password personal data policy')
->groups(['api', 'project'])
->label('scope', 'policies.write')
->label('scope', ['policies.write', 'project.policies.write'])
->label('event', 'projects.[projectId].policies.[policy].update')
->label('audits.event', 'projects.[projectId].policies.[policy].update')
->label('audits.resource', 'project/{response.$id}')
@@ -31,7 +31,7 @@ class Update extends Action
->httpAlias('/v1/projects/:projectId/auth/session-alerts')
->desc('Update session alert policy')
->groups(['api', 'project'])
->label('scope', 'policies.write')
->label('scope', ['policies.write', 'project.policies.write'])
->label('event', 'projects.[projectId].policies.[policy].update')
->label('audits.event', 'projects.[projectId].policies.[policy].update')
->label('audits.resource', 'project/{response.$id}')
@@ -31,7 +31,7 @@ class Update extends Action
->httpAlias('/v1/projects/:projectId/auth/duration')
->desc('Update session duration policy')
->groups(['api', 'project'])
->label('scope', 'policies.write')
->label('scope', ['policies.write', 'project.policies.write'])
->label('event', 'projects.[projectId].policies.[policy].update')
->label('audits.event', 'projects.[projectId].policies.[policy].update')
->label('audits.resource', 'project/{response.$id}')
@@ -31,7 +31,7 @@ class Update extends Action
->httpAlias('/v1/projects/:projectId/auth/session-invalidation')
->desc('Update session invalidation policy')
->groups(['api', 'project'])
->label('scope', 'policies.write')
->label('scope', ['policies.write', 'project.policies.write'])
->label('event', 'projects.[projectId].policies.[policy].update')
->label('audits.event', 'projects.[projectId].policies.[policy].update')
->label('audits.resource', 'project/{response.$id}')
@@ -32,7 +32,7 @@ class Update extends Action
->httpAlias('/v1/projects/:projectId/auth/max-sessions')
->desc('Update session limit policy')
->groups(['api', 'project'])
->label('scope', 'policies.write')
->label('scope', ['policies.write', 'project.policies.write'])
->label('event', 'projects.[projectId].policies.[policy].update')
->label('audits.event', 'projects.[projectId].policies.[policy].update')
->label('audits.resource', 'project/{response.$id}')
@@ -32,7 +32,7 @@ class Update extends Action
->httpAlias('/v1/projects/:projectId/auth/limit')
->desc('Update user limit policy')
->groups(['api', 'project'])
->label('scope', 'policies.write')
->label('scope', ['policies.write', 'project.policies.write'])
->label('event', 'projects.[projectId].policies.[policy].update')
->label('audits.event', 'projects.[projectId].policies.[policy].update')
->label('audits.resource', 'project/{response.$id}')
@@ -33,7 +33,7 @@ class XList extends Action
->setHttpPath('/v1/project/policies')
->desc('List project policies')
->groups(['api', 'project'])
->label('scope', 'policies.read')
->label('scope', ['policies.read', 'project.policies.read'])
->label('sdk', new Method(
namespace: 'project',
group: 'policies',
@@ -53,7 +53,7 @@ class Create extends Action
)
],
))
->param('variableId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject'])
->param('variableId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject'])
->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.')
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.')
->param('secret', true, new Boolean(), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true)
@@ -72,7 +72,7 @@ class Create extends Action
QueueEvent $queueForEvents,
Database $dbForProject,
) {
$variableId = ($variableId == 'unique()') ? ID::unique() : $variableId;
$variableId = ($variableId === 'unique()') ? ID::unique() : $variableId;
$variable = new Document([
'$id' => $variableId,
@@ -51,7 +51,7 @@ class Delete extends Action
],
contentType: ContentType::NONE
))
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID.', false, ['dbForProject'])
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
@@ -44,7 +44,7 @@ class Get extends Action
)
]
))
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID.', false, ['dbForProject'])
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
->callback($this->action(...));
@@ -52,7 +52,7 @@ class Update extends Action
)
]
))
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID.', false, ['dbForProject'])
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
->param('key', null, new Nullable(new Text(255, 0)), 'Variable key. Max length: 255 chars.', true)
->param('value', null, new Nullable(new Text(8192, 0)), 'Variable value. Max length: 8192 chars.', true)
->param('secret', null, new Nullable(new Boolean()), 'Secret variables can be updated or deleted, but only projects can read them during build and runtime.', true)
@@ -14,6 +14,7 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Logger\Log;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
@@ -43,12 +44,14 @@ class Create extends Action
->label('audits.resource', 'rule/{response.$id}')
->label('sdk', new Method(
namespace: 'proxy',
group: null,
group: 'rules',
name: 'createAPIRule',
description: <<<EOT
Create a new proxy rule for serving Appwrite's API on custom domain.
Rule ID is automatically generated as MD5 hash of a rule domain for performance purposes.
EOT,
auth: [AuthType::ADMIN],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
@@ -67,11 +70,21 @@ class Create extends Action
->inject('dbForPlatform')
->inject('platform')
->inject('log')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $domain, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, array $platform, Log $log)
{
public function action(
string $domain,
Response $response,
Document $project,
Certificate $publisherForCertificates,
Event $queueForEvents,
Database $dbForPlatform,
array $platform,
Log $log,
Authorization $authorization,
) {
$this->validateDomainRestrictions($domain, $platform);
// TODO: (@Meldiron) Remove after 1.7.x migration
@@ -108,7 +121,7 @@ class Create extends Action
}
try {
$rule = $dbForPlatform->createDocument('rules', $rule);
$rule = $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule));
} catch (Duplicate $e) {
throw new Exception(Exception::RULE_ALREADY_EXISTS);
}
@@ -126,6 +139,13 @@ class Create extends Action
$queueForEvents->setParam('ruleId', $rule->getId());
// Rename 'created' status to 'unverified' for consistency.
// 'verifying' and 'verified' statuses stay as is.
// 'unverified' in the meaning of failed certificate generation stays as is.
if ($rule->getAttribute('status') === 'created') {
$rule->setAttribute('status', 'unverified');
}
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($rule, Response::MODEL_PROXY_RULE);
@@ -12,6 +12,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
@@ -38,12 +39,12 @@ class Delete extends Action
->label('audits.resource', 'rule/{request.ruleId}')
->label('sdk', new Method(
namespace: 'proxy',
group: null,
group: 'rules',
name: 'deleteRule',
description: <<<EOT
Delete a proxy rule by its unique ID.
EOT,
auth: [AuthType::ADMIN],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
@@ -58,6 +59,7 @@ class Delete extends Action
->inject('dbForPlatform')
->inject('queueForDeletes')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
@@ -67,15 +69,16 @@ class Delete extends Action
Document $project,
Database $dbForPlatform,
DeleteEvent $queueForDeletes,
Event $queueForEvents
Event $queueForEvents,
Authorization $authorization,
) {
$rule = $dbForPlatform->getDocument('rules', $ruleId);
$rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $ruleId));
if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getSequence()) {
throw new Exception(Exception::RULE_NOT_FOUND);
}
$dbForPlatform->deleteDocument('rules', $rule->getId());
$authorization->skip(fn () => $dbForPlatform->deleteDocument('rules', $rule->getId()));
$queueForDeletes
->setType(DELETE_TYPE_DOCUMENT)
@@ -14,6 +14,7 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Logger\Log;
use Utopia\Platform\Scope\HTTP;
@@ -45,12 +46,14 @@ class Create extends Action
->label('audits.resource', 'rule/{response.$id}')
->label('sdk', new Method(
namespace: 'proxy',
group: null,
group: 'rules',
name: 'createFunctionRule',
description: <<<EOT
Create a new proxy rule for executing Appwrite Function on custom domain.
Rule ID is automatically generated as MD5 hash of a rule domain for performance purposes.
EOT,
auth: [AuthType::ADMIN],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
@@ -72,11 +75,25 @@ class Create extends Action
->inject('dbForProject')
->inject('platform')
->inject('log')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $domain, string $functionId, string $branch, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
{
public function action(
string $domain,
string $functionId,
string $branch,
Response $response,
Document $project,
Certificate $publisherForCertificates,
Event $queueForEvents,
Database $dbForPlatform,
Database $dbForProject,
array $platform,
Log $log,
Authorization $authorization,
) {
$this->validateDomainRestrictions($domain, $platform);
$function = $dbForProject->getDocument('functions', $functionId);
@@ -126,7 +143,7 @@ class Create extends Action
}
try {
$rule = $dbForPlatform->createDocument('rules', $rule);
$rule = $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule));
} catch (Duplicate $e) {
throw new Exception(Exception::RULE_ALREADY_EXISTS);
}
@@ -144,6 +161,13 @@ class Create extends Action
$queueForEvents->setParam('ruleId', $rule->getId());
// Rename 'created' status to 'unverified' for consistency.
// 'verifying' and 'verified' statuses stay as is.
// 'unverified' in the meaning of failed certificate generation stays as is.
if ($rule->getAttribute('status') === 'created') {
$rule->setAttribute('status', 'unverified');
}
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($rule, Response::MODEL_PROXY_RULE);
@@ -10,6 +10,7 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Scope\HTTP;
@@ -34,12 +35,12 @@ class Get extends Action
->label('scope', 'rules.read')
->label('sdk', new Method(
namespace: 'proxy',
group: null,
group: 'rules',
name: 'getRule',
description: <<<EOT
Get a proxy rule by its unique ID.
EOT,
auth: [AuthType::ADMIN],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -51,6 +52,7 @@ class Get extends Action
->inject('response')
->inject('project')
->inject('dbForPlatform')
->inject('authorization')
->callback($this->action(...));
}
@@ -58,15 +60,16 @@ class Get extends Action
string $ruleId,
Response $response,
Document $project,
Database $dbForPlatform
Database $dbForPlatform,
Authorization $authorization,
) {
$rule = $dbForPlatform->getDocument('rules', $ruleId);
$rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $ruleId));
if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getSequence()) {
throw new Exception(Exception::RULE_NOT_FOUND);
}
$certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', ''));
$certificate = $authorization->skip(fn () => $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', '')));
// Give priority to certificate generation logs if present
if (!empty($certificate->getAttribute('logs', ''))) {
@@ -75,6 +78,13 @@ class Get extends Action
$rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', ''));
// Rename 'created' status to 'unverified' for consistency.
// 'verifying' and 'verified' statuses stay as is.
// 'unverified' in the meaning of failed certificate generation stays as is.
if ($rule->getAttribute('status') === 'created') {
$rule->setAttribute('status', 'unverified');
}
$response->dynamic($rule, Response::MODEL_PROXY_RULE);
}
}
@@ -14,6 +14,7 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Logger\Log;
use Utopia\Platform\Scope\HTTP;
@@ -46,12 +47,14 @@ class Create extends Action
->label('audits.resource', 'rule/{response.$id}')
->label('sdk', new Method(
namespace: 'proxy',
group: null,
group: 'rules',
name: 'createRedirectRule',
description: <<<EOT
Create a new proxy rule for to redirect from custom domain to another domain.
Rule ID is automatically generated as MD5 hash of a rule domain for performance purposes.
EOT,
auth: [AuthType::ADMIN],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
@@ -75,11 +78,27 @@ class Create extends Action
->inject('dbForProject')
->inject('platform')
->inject('log')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $domain, string $url, int $statusCode, string $resourceId, string $resourceType, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
{
public function action(
string $domain,
string $url,
int $statusCode,
string $resourceId,
string $resourceType,
Response $response,
Document $project,
Certificate $publisherForCertificates,
Event $queueForEvents,
Database $dbForPlatform,
Database $dbForProject,
array $platform,
Log $log,
Authorization $authorization,
) {
$this->validateDomainRestrictions($domain, $platform);
$collection = match ($resourceType) {
@@ -131,7 +150,7 @@ class Create extends Action
}
try {
$rule = $dbForPlatform->createDocument('rules', $rule);
$rule = $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule));
} catch (Duplicate $e) {
throw new Exception(Exception::RULE_ALREADY_EXISTS);
}
@@ -149,6 +168,13 @@ class Create extends Action
$queueForEvents->setParam('ruleId', $rule->getId());
// Rename 'created' status to 'unverified' for consistency.
// 'verifying' and 'verified' statuses stay as is.
// 'unverified' in the meaning of failed certificate generation stays as is.
if ($rule->getAttribute('status') === 'created') {
$rule->setAttribute('status', 'unverified');
}
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($rule, Response::MODEL_PROXY_RULE);
@@ -14,6 +14,7 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Logger\Log;
use Utopia\Platform\Scope\HTTP;
@@ -45,12 +46,14 @@ class Create extends Action
->label('audits.resource', 'rule/{response.$id}')
->label('sdk', new Method(
namespace: 'proxy',
group: null,
group: 'rules',
name: 'createSiteRule',
description: <<<EOT
Create a new proxy rule for serving Appwrite Site on custom domain.
Rule ID is automatically generated as MD5 hash of a rule domain for performance purposes.
EOT,
auth: [AuthType::ADMIN],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_CREATED,
@@ -72,11 +75,25 @@ class Create extends Action
->inject('dbForProject')
->inject('platform')
->inject('log')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $domain, string $siteId, ?string $branch, Response $response, Document $project, Certificate $publisherForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log)
{
public function action(
string $domain,
string $siteId,
?string $branch,
Response $response,
Document $project,
Certificate $publisherForCertificates,
Event $queueForEvents,
Database $dbForPlatform,
Database $dbForProject,
array $platform,
Log $log,
Authorization $authorization,
) {
$this->validateDomainRestrictions($domain, $platform);
$site = $dbForProject->getDocument('sites', $siteId);
@@ -126,7 +143,7 @@ class Create extends Action
}
try {
$rule = $dbForPlatform->createDocument('rules', $rule);
$rule = $authorization->skip(fn () => $dbForPlatform->createDocument('rules', $rule));
} catch (Duplicate $e) {
throw new Exception(Exception::RULE_ALREADY_EXISTS);
}
@@ -144,6 +161,13 @@ class Create extends Action
$queueForEvents->setParam('ruleId', $rule->getId());
// Rename 'created' status to 'unverified' for consistency.
// 'verifying' and 'verified' statuses stay as is.
// 'unverified' in the meaning of failed certificate generation stays as is.
if ($rule->getAttribute('status') === 'created') {
$rule->setAttribute('status', 'unverified');
}
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($rule, Response::MODEL_PROXY_RULE);
@@ -1,6 +1,6 @@
<?php
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Verification;
namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Status;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Certificate;
@@ -13,6 +13,7 @@ use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Logger\Log;
use Utopia\Platform\Scope\HTTP;
@@ -32,8 +33,9 @@ class Update extends Action
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/proxy/rules/:ruleId/verification')
->desc('Update rule verification status')
->setHttpPath('/v1/proxy/rules/:ruleId/status')
->httpAlias('/v1/proxy/rules/:ruleId/verification')
->desc('Update rule status')
->groups(['api', 'proxy'])
->label('scope', 'rules.write')
->label('event', 'rules.[ruleId].update')
@@ -41,12 +43,12 @@ class Update extends Action
->label('audits.resource', 'rule/{response.$id}')
->label('sdk', new Method(
namespace: 'proxy',
group: null,
name: 'updateRuleVerification',
group: 'rules',
name: 'updateRuleStatus',
description: <<<EOT
Retry getting verification process of a proxy rule. This endpoint triggers domain verification by checking DNS records (CNAME) against the configured target domain. If verification is successful, a TLS certificate will be automatically provisioned for the domain.
If not succeeded yet, retry verification process of a proxy rule domain. This endpoint triggers domain verification by checking DNS records. If verification is successful, a TLS certificate will be automatically provisioned for the domain asynchronously in the background.
EOT,
auth: [AuthType::ADMIN],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -61,6 +63,7 @@ class Update extends Action
->inject('project')
->inject('dbForPlatform')
->inject('log')
->inject('authorization')
->callback($this->action(...));
}
@@ -71,9 +74,10 @@ class Update extends Action
Event $queueForEvents,
Document $project,
Database $dbForPlatform,
Log $log
Log $log,
Authorization $authorization,
) {
$rule = $dbForPlatform->getDocument('rules', $ruleId);
$rule = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $ruleId));
if ($rule->isEmpty() || $rule->getAttribute('projectInternalId') !== $project->getSequence()) {
throw new Exception(Exception::RULE_NOT_FOUND);
@@ -90,22 +94,22 @@ class Update extends Action
try {
$this->verifyRule($rule, $log);
// Reset logs and status for the rule
$rule = $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([
$rule = $authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([
'logs' => '',
'status' => RULE_STATUS_CERTIFICATE_GENERATING,
]));
])));
$certificateId = $rule->getAttribute('certificateId', '');
// Reset logs for the associated certificate.
if (!empty($certificateId)) {
$certificate = $dbForPlatform->updateDocument('certificates', $certificateId, new Document([
$certificate = $authorization->skip(fn () => $dbForPlatform->updateDocument('certificates', $certificateId, new Document([
'logs' => '',
]));
])));
}
} catch (Exception $err) {
$dbForPlatform->updateDocument('rules', $rule->getId(), new Document([
$authorization->skip(fn () => $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([
'$updatedAt' => DateTime::now(),
]));
])));
throw $err;
}
@@ -13,6 +13,7 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
@@ -39,12 +40,12 @@ class XList extends Action
->label('scope', 'rules.read')
->label('sdk', new Method(
namespace: 'proxy',
group: null,
group: 'rules',
name: 'listRules',
description: <<<EOT
Get a list of all the proxy rules. You can use the query params to filter your results.
EOT,
auth: [AuthType::ADMIN],
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
@@ -53,21 +54,23 @@ class XList extends Action
]
))
->param('queries', [], new Rules(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/databases#querying-documents). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Rules::ALLOWED_ATTRIBUTES), true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true, deprecated: true)
->inject('response')
->inject('project')
->inject('dbForPlatform')
->inject('authorization')
->callback($this->action(...));
}
public function action(
array $queries,
bool $total,
string $search,
bool $includeTotal,
Response $response,
Document $project,
Database $dbForPlatform
Database $dbForPlatform,
Authorization $authorization,
) {
try {
$queries = Query::parseQueries($queries);
@@ -91,7 +94,7 @@ class XList extends Action
}
$ruleId = $cursor->getValue();
$cursorDocument = $dbForPlatform->getDocument('rules', $ruleId);
$cursorDocument = $authorization->skip(fn () => $dbForPlatform->getDocument('rules', $ruleId));
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Rule '{$ruleId}' for the 'cursor' value not found.");
@@ -102,9 +105,9 @@ class XList extends Action
$filterQueries = Query::groupByType($queries)['filters'];
$rules = $dbForPlatform->find('rules', $queries);
$rules = $authorization->skip(fn () => $dbForPlatform->find('rules', $queries));
foreach ($rules as $rule) {
$certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', ''));
$certificate = $authorization->skip(fn () => $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', '')));
// Give priority to certificate generation logs if present
if (!empty($certificate->getAttribute('logs', ''))) {
@@ -112,11 +115,18 @@ class XList extends Action
}
$rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', ''));
// Rename 'created' status to 'unverified' for consistency.
// 'verifying' and 'verified' statuses stay as is.
// 'unverified' in the meaning of failed certificate generation stays as is.
if ($rule->getAttribute('status') === 'created') {
$rule->setAttribute('status', 'unverified');
}
}
$response->dynamic(new Document([
'rules' => $rules,
'total' => $includeTotal ? $dbForPlatform->count('rules', $filterQueries, APP_LIMIT_COUNT) : 0,
'total' => $total ? $authorization->skip(fn () => $dbForPlatform->count('rules', $filterQueries, APP_LIMIT_COUNT)) : 0,
]), Response::MODEL_PROXY_RULE_LIST);
}
}
@@ -8,7 +8,7 @@ use Appwrite\Platform\Modules\Proxy\Http\Rules\Function\Create as CreateFunction
use Appwrite\Platform\Modules\Proxy\Http\Rules\Get as GetRule;
use Appwrite\Platform\Modules\Proxy\Http\Rules\Redirect\Create as CreateRedirectRule;
use Appwrite\Platform\Modules\Proxy\Http\Rules\Site\Create as CreateSiteRule;
use Appwrite\Platform\Modules\Proxy\Http\Rules\Verification\Update as UpdateRuleVerification;
use Appwrite\Platform\Modules\Proxy\Http\Rules\Status\Update as UpdateRuleStatus;
use Appwrite\Platform\Modules\Proxy\Http\Rules\XList as ListRules;
use Utopia\Platform\Service;
@@ -26,6 +26,6 @@ class Http extends Service
$this->addAction(GetRule::getName(), new GetRule());
$this->addAction(ListRules::getName(), new ListRules());
$this->addAction(DeleteRule::getName(), new DeleteRule());
$this->addAction(UpdateRuleVerification::getName(), new UpdateRuleVerification());
$this->addAction(UpdateRuleStatus::getName(), new UpdateRuleStatus());
}
}
@@ -2,11 +2,13 @@
namespace Appwrite\Platform\Modules\Sites\Http\Variables;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
@@ -36,6 +38,7 @@ class Create extends Base
->groups(['api', 'sites'])
->label('scope', 'sites.write')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('event', 'variables.[variableId].create')
->label('audits.event', 'variable.create')
->label('audits.resource', 'site/{request.siteId}')
->label('sdk', new Method(
@@ -54,16 +57,18 @@ class Create extends Base
]
))
->param('siteId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Site unique ID.', false, ['dbForProject'])
->param('variableId', '', fn (Database $dbForProject) => new CustomId(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Variable ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', false, ['dbForProject'])
->param('key', null, new Text(Database::LENGTH_KEY), 'Variable key. Max length: ' . Database::LENGTH_KEY . ' chars.', false)
->param('value', null, new Text(8192, 0), 'Variable value. Max length: 8192 chars.', false)
->param('secret', true, new Boolean(), 'Secret variables can be updated or deleted, but only sites can read them during build and runtime.', true)
->inject('response')
->inject('queueForEvents')
->inject('dbForProject')
->inject('project')
->callback($this->action(...));
}
public function action(string $siteId, string $key, string $value, bool $secret, Response $response, Database $dbForProject, Document $project)
public function action(string $siteId, string $variableId, string $key, string $value, bool $secret, Response $response, QueueEvent $queueForEvents, Database $dbForProject, Document $project)
{
$site = $dbForProject->getDocument('sites', $siteId);
@@ -71,7 +76,7 @@ class Create extends Base
throw new Exception(Exception::SITE_NOT_FOUND);
}
$variableId = ID::unique();
$variableId = ($variableId === 'unique()') ? ID::unique() : $variableId;
$teamId = $project->getAttribute('teamId', '');
$variable = new Document([
@@ -96,6 +101,8 @@ class Create extends Base
'live' => false,
]));
$queueForEvents->setParam('variableId', $variable->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($variable, Response::MODEL_VARIABLE);
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Modules\Sites\Http\Variables;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
@@ -33,6 +34,7 @@ class Delete extends Base
->groups(['api', 'sites'])
->label('scope', 'sites.write')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('event', 'variables.[variableId].delete')
->label('audits.event', 'variable.delete')
->label('audits.resource', 'site/{request.siteId}')
->label('sdk', new Method(
@@ -54,11 +56,12 @@ class Delete extends Base
->param('siteId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Site unique ID.', false, ['dbForProject'])
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
->inject('response')
->inject('queueForEvents')
->inject('dbForProject')
->callback($this->action(...));
}
public function action(string $siteId, string $variableId, Response $response, Database $dbForProject)
public function action(string $siteId, string $variableId, Response $response, QueueEvent $queueForEvents, Database $dbForProject)
{
$site = $dbForProject->getDocument('sites', $siteId);
@@ -77,6 +80,8 @@ class Delete extends Base
'live' => false,
]));
$queueForEvents->setParam('variableId', $variable->getId());
$response->noContent();
}
}
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Modules\Sites\Http\Variables;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
@@ -35,6 +36,7 @@ class Update extends Base
->desc('Update variable')
->groups(['api', 'sites'])
->label('scope', 'sites.write')
->label('event', 'variables.[variableId].update')
->label('audits.event', 'variable.update')
->label('audits.resource', 'site/{request.siteId}')
->label('resourceType', RESOURCE_TYPE_SITES)
@@ -55,10 +57,11 @@ class Update extends Base
))
->param('siteId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Site unique ID.', false, ['dbForProject'])
->param('variableId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Variable unique ID.', false, ['dbForProject'])
->param('key', null, new Text(255), 'Variable key. Max length: 255 chars.', false)
->param('key', null, new Nullable(new Text(255, 0)), 'Variable key. Max length: 255 chars.', true)
->param('value', null, new Nullable(new Text(8192, 0)), 'Variable value. Max length: 8192 chars.', true)
->param('secret', null, new Nullable(new Boolean()), 'Secret variables can be updated or deleted, but only sites can read them during build and runtime.', true)
->inject('response')
->inject('queueForEvents')
->inject('dbForProject')
->callback($this->action(...));
}
@@ -66,10 +69,11 @@ class Update extends Base
public function action(
string $siteId,
string $variableId,
string $key,
?string $key,
?string $value,
?bool $secret,
Response $response,
QueueEvent $queueForEvents,
Database $dbForProject
) {
$site = $dbForProject->getDocument('sites', $siteId);
@@ -87,19 +91,27 @@ class Update extends Base
throw new Exception(Exception::VARIABLE_CANNOT_UNSET_SECRET);
}
$variable
->setAttribute('key', $key)
->setAttribute('value', $value ?? $variable->getAttribute('value'))
->setAttribute('secret', $secret ?? $variable->getAttribute('secret'))
->setAttribute('search', implode(' ', [$variableId, $site->getId(), $key, 'site']));
if (\is_null($key) && \is_null($value) && \is_null($secret)) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID);
}
$updates = new Document();
if (!\is_null($key)) {
$updates->setAttribute('key', $key);
$updates->setAttribute('search', implode(' ', [$variableId, $site->getId(), $key, 'site']));
}
if (!\is_null($value)) {
$updates->setAttribute('value', $value);
}
if (!\is_null($secret)) {
$updates->setAttribute('secret', $secret);
}
try {
$dbForProject->updateDocument('variables', $variable->getId(), new Document([
'key' => $variable->getAttribute('key'),
'value' => $variable->getAttribute('value'),
'secret' => $variable->getAttribute('secret'),
'search' => $variable->getAttribute('search'),
]));
$variable = $dbForProject->updateDocument('variables', $variable->getId(), $updates);
} catch (DuplicateException $th) {
throw new Exception(Exception::VARIABLE_ALREADY_EXISTS);
}
@@ -108,6 +120,8 @@ class Update extends Base
'live' => false,
]));
$queueForEvents->setParam('variableId', $variable->getId());
$response->dynamic($variable, Response::MODEL_VARIABLE);
}
}
@@ -7,12 +7,18 @@ use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Queries\Variables;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Order as OrderException;
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Query;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
class XList extends Base
{
@@ -51,13 +57,20 @@ class XList extends Base
)
)
->param('siteId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Site unique ID.', false, ['dbForProject'])
->param('queries', [], new Variables(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Variables::ALLOWED_ATTRIBUTES), true)
->param('total', true, new Boolean(true), 'When set to false, the total count returned will be 0 and will not be calculated.', true)
->inject('response')
->inject('dbForProject')
->callback($this->action(...));
}
/**
* @param array<string> $queries
*/
public function action(
string $siteId,
array $queries,
bool $includeTotal,
Response $response,
Database $dbForProject
) {
@@ -67,9 +80,51 @@ class XList extends Base
throw new Exception(Exception::SITE_NOT_FOUND);
}
try {
$queries = Query::parseQueries($queries);
} catch (QueryException $e) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$queries[] = Query::equal('resourceType', ['site']);
$queries[] = Query::equal('resourceInternalId', [$site->getSequence()]);
$queries[] = Query::orderAsc();
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
if ($cursor !== false) {
$validator = new Cursor();
if (!$validator->isValid($cursor)) {
throw new Exception(Exception::GENERAL_QUERY_INVALID, $validator->getDescription());
}
$variableId = $cursor->getValue();
$cursorDocument = $dbForProject->findOne('variables', [
Query::equal('$id', [$variableId]),
Query::equal('resourceType', ['site']),
Query::equal('resourceInternalId', [$site->getSequence()]),
]);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Variable '{$variableId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$filterQueries = Query::groupByType($queries)['filters'];
try {
$variables = $dbForProject->find('variables', $queries);
$total = $includeTotal ? $dbForProject->count('variables', $filterQueries, APP_LIMIT_COUNT) : 0;
} catch (OrderException $e) {
throw new Exception(Exception::DATABASE_QUERY_ORDER_NULL, "The order attribute '{$e->getAttribute()}' had a null value. Cursor pagination requires all documents order attribute values are non-null.");
}
$response->dynamic(new Document([
'variables' => $site->getAttribute('vars', []),
'total' => \count($site->getAttribute('vars', [])),
'variables' => $variables,
'total' => $total,
]), Response::MODEL_VARIABLE_LIST);
}
}
+31 -1
View File
@@ -163,6 +163,12 @@ class Specs extends Action
'description' => 'Your secret dev API key',
'in' => 'header',
],
'Cookie' => [
'type' => 'apiKey',
'name' => 'Cookie',
'description' => 'The user cookie to authenticate with. Used by SDKs that forward an incoming Cookie header in server-side runtimes.',
'in' => 'header',
],
'ImpersonateUserId' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Impersonate-User-Id',
@@ -219,6 +225,18 @@ class Specs extends Action
'description' => 'The user agent string of the client that made the request',
'in' => 'header',
],
'DevKey' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Dev-Key',
'description' => 'Your secret dev API key',
'in' => 'header',
],
'Cookie' => [
'type' => 'apiKey',
'name' => 'Cookie',
'description' => 'The user cookie to authenticate with. Used by SDKs that forward an incoming Cookie header in server-side runtimes.',
'in' => 'header',
],
'ImpersonateUserId' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Impersonate-User-Id',
@@ -272,7 +290,19 @@ class Specs extends Action
'Cookie' => [
'type' => 'apiKey',
'name' => 'Cookie',
'description' => 'The user cookie to authenticate with',
'description' => 'The user cookie to authenticate with. Used by SDKs that forward an incoming Cookie header in server-side runtimes.',
'in' => 'header',
],
'Session' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Session',
'description' => 'The user session to authenticate with',
'in' => 'header',
],
'DevKey' => [
'type' => 'apiKey',
'name' => 'X-Appwrite-Dev-Key',
'description' => 'Your secret dev API key',
'in' => 'header',
],
'ImpersonateUserId' => [
+23 -18
View File
@@ -10,6 +10,7 @@ use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Utopia\Response\Model\Execution;
use Executor\Exception\Timeout as ExecutorTimeout;
use Executor\Executor;
use Utopia\Bus\Bus;
use Utopia\Config\Config;
@@ -565,24 +566,28 @@ class Functions extends Action
Span::add('trigger', $trigger);
Span::current()?->finish();
}
$executionResponse = $executor->createExecution(
projectId: $project->getId(),
deploymentId: $deploymentId,
body: \strlen($body) > 0 ? $body : null,
variables: $vars,
timeout: $function->getAttribute('timeout', 0),
image: $runtime['image'],
source: $source,
entrypoint: $deployment->getAttribute('entrypoint', ''),
version: $version,
path: $path,
method: $method,
headers: $headers,
runtimeEntrypoint: $command,
cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT,
memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT,
logging: $function->getAttribute('logging', true),
);
try {
$executionResponse = $executor->createExecution(
projectId: $project->getId(),
deploymentId: $deploymentId,
body: \strlen($body) > 0 ? $body : null,
variables: $vars,
timeout: $function->getAttribute('timeout', 0),
image: $runtime['image'],
source: $source,
entrypoint: $deployment->getAttribute('entrypoint', ''),
version: $version,
path: $path,
method: $method,
headers: $headers,
runtimeEntrypoint: $command,
cpus: $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT,
memory: $spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT,
logging: $function->getAttribute('logging', true),
);
} catch (ExecutorTimeout $th) {
throw new AppwriteException(AppwriteException::FUNCTION_ASYNCHRONOUS_TIMEOUT, previous: $th);
}
$status = $executionResponse['statusCode'] >= 500 ? 'failed' : 'completed';
+52 -42
View File
@@ -30,6 +30,7 @@ use Utopia\Migration\Destination;
use Utopia\Migration\Destinations\Appwrite as DestinationAppwrite;
use Utopia\Migration\Destinations\CSV as DestinationCSV;
use Utopia\Migration\Destinations\JSON as DestinationJSON;
use Utopia\Migration\Destinations\OnDuplicate;
use Utopia\Migration\Exception as MigrationException;
use Utopia\Migration\Resource;
use Utopia\Migration\Resources\Database\Database as ResourceDatabase;
@@ -291,6 +292,7 @@ class Migrations extends Action
$this->dbForProject,
$this->getDatabasesDB,
Config::getParam('collections', [])['databases']['collections'],
OnDuplicate::tryFrom($options['onDuplicate'] ?? '') ?? OnDuplicate::Fail,
),
DestinationCSV::getName() => new DestinationCSV(
$this->deviceForFiles,
@@ -338,6 +340,55 @@ class Migrations extends Action
);
}
/**
* @return array<string>
*/
protected function getAPIKeyScopes(): array
{
return [
'users.read',
'users.write',
'teams.read',
'teams.write',
'buckets.read',
'buckets.write',
'files.read',
'files.write',
'functions.read',
'functions.write',
'sites.read',
'sites.write',
'tokens.read',
'tokens.write',
'providers.read',
'providers.write',
'topics.read',
'topics.write',
'subscribers.read',
'subscribers.write',
'messages.read',
'messages.write',
'targets.read',
'targets.write',
'webhooks.read',
'webhooks.write',
'project.read',
'project.write',
'keys.read',
'keys.write',
'platforms.read',
'platforms.write',
'oauth2.read',
'oauth2.write',
'mocks.read',
'mocks.write',
'project.policies.read',
'project.policies.write',
'templates.read',
'templates.write',
];
}
/**
* @throws Exception
*/
@@ -358,48 +409,7 @@ class Migrations extends Action
METRIC_NETWORK_INBOUND,
METRIC_NETWORK_OUTBOUND,
],
'scopes' => [
'users.read',
'users.write',
'teams.read',
'teams.write',
'buckets.read',
'buckets.write',
'files.read',
'files.write',
'functions.read',
'functions.write',
'sites.read',
'sites.write',
'tokens.read',
'tokens.write',
'providers.read',
'providers.write',
'topics.read',
'topics.write',
'subscribers.read',
'subscribers.write',
'messages.read',
'messages.write',
'targets.read',
'targets.write',
'webhooks.read',
'webhooks.write',
'project.read',
'project.write',
'keys.read',
'keys.write',
'platforms.read',
'platforms.write',
'oauth2.read',
'oauth2.write',
'mocks.read',
'mocks.write',
'policies.read',
'policies.write',
'templates.read',
'templates.write',
]
'scopes' => $this->getAPIKeyScopes(),
]);
return API_KEY_EPHEMERAL . '_' . $apiKey;
+29
View File
@@ -68,6 +68,17 @@ abstract class Format
'mock-unverified'
],
],
[
'namespace' => 'project',
'methods' => [
'getOAuth2Provider'
],
'parameter' => 'providerId',
'excludeKeys' => [
'mock',
'mock-unverified'
],
],
];
/**
@@ -743,6 +754,24 @@ abstract class Format
break;
case 'project':
switch ($method) {
case 'updateAuthMethod':
switch ($param) {
case 'methodId':
return 'AuthMethod';
}
break;
case 'getPolicy':
switch ($param) {
case 'policyId':
return 'ProjectPolicy';
}
break;
case 'getOAuth2Provider':
switch ($param) {
case 'providerId':
return 'OAuthProvider';
}
break;
case 'getEmailTemplate':
case 'updateEmailTemplate':
switch ($param) {
@@ -437,6 +437,15 @@ class OpenAPI3 extends Format
$node['schema']['type'] = $validator->getType();
$node['schema']['x-example'] = ($param['example'] ?? '') ?: '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>';
break;
case \Utopia\Database\Validator\BigInt::class:
// BigInt validator reports Database::VAR_BIGINT, but OpenAPI expects scalar types.
// We expose it as int64 to keep schema consistent with Column/Attribute models.
$node['schema']['type'] = 'integer';
$node['schema']['format'] = 'int64';
if (!empty($param['example'])) {
$node['schema']['x-example'] = $param['example'];
}
break;
case \Utopia\Validator\Boolean::class:
$node['schema']['type'] = $validator->getType();
$node['schema']['x-example'] = ($param['example'] ?? '') ?: false;
@@ -746,7 +755,18 @@ class OpenAPI3 extends Format
$node['schema']['default'] = $param['default'];
}
if (false !== \strpos($url, ':' . $name)) { // Param is in URL path
$pathAliases = [$name, ...($param['aliases'] ?? [])];
$pathAliasMap = \array_flip($pathAliases);
$isPathParam = false;
foreach (\explode('/', $url) as $segment) {
if ($segment !== '' && $segment[0] === ':' && isset($pathAliasMap[\substr($segment, 1)])) {
$isPathParam = true;
break;
}
}
if ($isPathParam) { // Param is in URL path (directly or through alias)
$node['in'] = 'path';
$temp['parameters'][] = $node;
} elseif ($route->getMethod() == 'GET') { // Param is in query
@@ -787,7 +807,14 @@ class OpenAPI3 extends Format
}
}
$url = \str_replace(':' . $name, '{' . $name . '}', $url);
$segments = \explode('/', $url);
foreach ($segments as &$segment) {
if ($segment !== '' && $segment[0] === ':' && isset($pathAliasMap[\substr($segment, 1)])) {
$segment = '{' . $name . '}';
}
}
unset($segment);
$url = \implode('/', $segments);
}
if (!empty($bodyRequired)) {
@@ -722,7 +722,18 @@ class Swagger2 extends Format
$node['default'] = $param['default'];
}
if (\str_contains($url, ':' . $name)) { // Param is in URL path
$pathAliases = [$name, ...($param['aliases'] ?? [])];
$pathAliasMap = \array_flip($pathAliases);
$isPathParam = false;
foreach (\explode('/', $url) as $segment) {
if ($segment !== '' && $segment[0] === ':' && isset($pathAliasMap[\substr($segment, 1)])) {
$isPathParam = true;
break;
}
}
if ($isPathParam) { // Param is in URL path (directly or through alias)
$node['in'] = 'path';
$temp['parameters'][] = $node;
} elseif ($route->getMethod() == 'GET') { // Param is in query
@@ -767,7 +778,14 @@ class Swagger2 extends Format
}
}
$url = \str_replace(':' . $name, '{' . $name . '}', $url);
$segments = \explode('/', $url);
foreach ($segments as &$segment) {
if ($segment !== '' && $segment[0] === ':' && isset($pathAliasMap[\substr($segment, 1)])) {
$segment = '{' . $name . '}';
}
}
unset($segment);
$url = \implode('/', $segments);
}
if (!empty($bodyRequired)) {
@@ -23,6 +23,7 @@ class Attributes extends Validator
protected array $supportedTypes = [
Database::VAR_STRING,
Database::VAR_INTEGER,
Database::VAR_BIGINT,
Database::VAR_FLOAT,
Database::VAR_BOOLEAN,
Database::VAR_DATETIME,
@@ -181,9 +182,9 @@ class Attributes extends Validator
return false;
}
// Validate signed only for integer/float types
if (isset($attribute['signed']) && !in_array($attribute['type'], [Database::VAR_INTEGER, Database::VAR_FLOAT])) {
$this->message = "Attribute '" . $attribute['key'] . "': 'signed' can only be used with integer or float types";
// Validate signed only for integer/bigint/float types
if (isset($attribute['signed']) && !in_array($attribute['type'], [Database::VAR_INTEGER, Database::VAR_BIGINT, Database::VAR_FLOAT])) {
$this->message = "Attribute '" . $attribute['key'] . "': 'signed' can only be used with integer, bigint or float types";
return false;
}
@@ -199,10 +200,10 @@ class Attributes extends Validator
return false;
}
// Validate min/max range for integer/float
// Validate min/max range for integer/bigint/float
if (isset($attribute['min']) || isset($attribute['max'])) {
if (!in_array($attribute['type'], [Database::VAR_INTEGER, Database::VAR_FLOAT])) {
$this->message = "Attribute '" . $attribute['key'] . "': min/max can only be used with integer or float types";
if (!in_array($attribute['type'], [Database::VAR_INTEGER, Database::VAR_BIGINT, Database::VAR_FLOAT])) {
$this->message = "Attribute '" . $attribute['key'] . "': min/max can only be used with integer, bigint or float types";
return false;
}
@@ -264,7 +265,7 @@ class Attributes extends Validator
if (isset($attribute['min']) || isset($attribute['max'])) {
$min = $attribute['min'] ?? \PHP_INT_MIN;
$max = $attribute['max'] ?? \PHP_INT_MAX;
$rangeValidator = new Range($min, $max, Database::VAR_INTEGER);
$rangeValidator = new Range($min, $max, Range::TYPE_INTEGER);
if (!$rangeValidator->isValid($attribute['default'])) {
$this->message = "Default value for integer attribute '" . $attribute['key'] . "' must be between $min and $max";
return false;
@@ -272,6 +273,23 @@ class Attributes extends Validator
}
break;
case Database::VAR_BIGINT:
if (!is_int($attribute['default'])) {
$this->message = "Default value for bigint attribute '" . $attribute['key'] . "' must be an integer";
return false;
}
// Validate within range if min/max specified
if (isset($attribute['min']) || isset($attribute['max'])) {
$min = $attribute['min'] ?? \PHP_INT_MIN;
$max = $attribute['max'] ?? \PHP_INT_MAX;
$rangeValidator = new Range($min, $max, Range::TYPE_INTEGER);
if (!$rangeValidator->isValid($attribute['default'])) {
$this->message = "Default value for bigint attribute '" . $attribute['key'] . "' must be between $min and $max";
return false;
}
}
break;
case Database::VAR_FLOAT:
if (!is_float($attribute['default']) && !is_int($attribute['default'])) {
$this->message = "Default value for float attribute '" . $attribute['key'] . "' must be a number";
@@ -281,7 +299,7 @@ class Attributes extends Validator
if (isset($attribute['min']) || isset($attribute['max'])) {
$min = $attribute['min'] ?? -\PHP_FLOAT_MAX;
$max = $attribute['max'] ?? \PHP_FLOAT_MAX;
$rangeValidator = new Range($min, $max, Database::VAR_FLOAT);
$rangeValidator = new Range($min, $max, Range::TYPE_FLOAT);
if (!$rangeValidator->isValid((float)$attribute['default'])) {
$this->message = "Default value for float attribute '" . $attribute['key'] . "' must be between $min and $max";
return false;
@@ -0,0 +1,27 @@
<?php
namespace Appwrite\Utopia\Request\Filters;
use Appwrite\Utopia\Request\Filter;
class V25 extends Filter
{
// Convert 1.9.3 params to 1.9.4
public function parse(array $content, string $model): array
{
switch ($model) {
case 'functions.createVariable':
case 'sites.createVariable':
$content = $this->fillVariableId($content);
break;
}
return $content;
}
protected function fillVariableId(array $content): array
{
$content['variableId'] = $content['variableId'] ?? 'unique()';
return $content;
}
}
+2
View File
@@ -72,6 +72,7 @@ class Response extends SwooleResponse
public const MODEL_ATTRIBUTE_LIST = 'attributeList';
public const MODEL_ATTRIBUTE_STRING = 'attributeString';
public const MODEL_ATTRIBUTE_INTEGER = 'attributeInteger';
public const MODEL_ATTRIBUTE_BIGINT = 'attributeBigint';
public const MODEL_ATTRIBUTE_FLOAT = 'attributeFloat';
public const MODEL_ATTRIBUTE_BOOLEAN = 'attributeBoolean';
public const MODEL_ATTRIBUTE_EMAIL = 'attributeEmail';
@@ -95,6 +96,7 @@ class Response extends SwooleResponse
public const MODEL_COLUMN_LIST = 'columnList';
public const MODEL_COLUMN_STRING = 'columnString';
public const MODEL_COLUMN_INTEGER = 'columnInteger';
public const MODEL_COLUMN_BIGINT = 'columnBigint';
public const MODEL_COLUMN_FLOAT = 'columnFloat';
public const MODEL_COLUMN_BOOLEAN = 'columnBoolean';
public const MODEL_COLUMN_EMAIL = 'columnEmail';
@@ -0,0 +1,34 @@
<?php
namespace Appwrite\Utopia\Response\Filters;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Filter;
// Convert 1.9.4 Data format to 1.9.3 format
class V25 extends Filter
{
public function parse(array $content, string $model): array
{
return match ($model) {
Response::MODEL_OAUTH2_OIDC => $this->parseOAuth2Oidc($content),
Response::MODEL_OAUTH2_PROVIDER_LIST => $this->handleList($content, 'providers', fn ($item) => ($item['$id'] ?? null) === 'oidc' ? $this->parseOAuth2Oidc($item) : $item),
default => $content,
};
}
private function parseOAuth2Oidc(array $content): array
{
if (isset($content['tokenURL'])) {
$content['tokenUrl'] = $content['tokenURL'];
unset($content['tokenURL']);
}
if (isset($content['userInfoURL'])) {
$content['userInfoUrl'] = $content['userInfoURL'];
unset($content['userInfoURL']);
}
return $content;
}
}
@@ -0,0 +1,66 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class AttributeBigInt extends Attribute
{
public function __construct()
{
parent::__construct();
$this
->addRule('key', [
'type' => self::TYPE_STRING,
'description' => 'Attribute Key.',
'default' => '',
'example' => 'count',
])
->addRule('type', [
'type' => self::TYPE_STRING,
'description' => 'Attribute type.',
'default' => '',
'example' => 'bigint',
])
->addRule('min', [
'type' => self::TYPE_INTEGER,
'format' => 'int64',
'description' => 'Minimum value to enforce for new documents.',
'default' => null,
'required' => false,
'example' => 1,
])
->addRule('max', [
'type' => self::TYPE_INTEGER,
'format' => 'int64',
'description' => 'Maximum value to enforce for new documents.',
'default' => null,
'required' => false,
'example' => 10,
])
->addRule('default', [
'type' => self::TYPE_INTEGER,
'format' => 'int64',
'description' => 'Default value for attribute when not provided. Cannot be set when attribute is required.',
'default' => null,
'required' => false,
'example' => 10,
])
;
}
public array $conditions = [
'type' => 'bigint'
];
public function getName(): string
{
return 'AttributeBigInt';
}
public function getType(): string
{
return Response::MODEL_ATTRIBUTE_BIGINT;
}
}
@@ -19,6 +19,9 @@ class AttributeList extends Model
->addRule('attributes', [
'type' => [
Response::MODEL_ATTRIBUTE_BOOLEAN,
// BigInt must come before Integer: response model dispatch is "first match wins",
// and Integer matches all int types (including bigint), while BigInt is more specific (size=8).
Response::MODEL_ATTRIBUTE_BIGINT,
Response::MODEL_ATTRIBUTE_INTEGER,
Response::MODEL_ATTRIBUTE_FLOAT,
Response::MODEL_ATTRIBUTE_EMAIL,
@@ -62,6 +62,9 @@ class Collection extends Model
->addRule('attributes', [
'type' => [
Response::MODEL_ATTRIBUTE_BOOLEAN,
// BigInt must come before Integer: response model dispatch is "first match wins",
// and Integer matches all int types (including bigint), while BigInt is more specific (size=8).
Response::MODEL_ATTRIBUTE_BIGINT,
Response::MODEL_ATTRIBUTE_INTEGER,
Response::MODEL_ATTRIBUTE_FLOAT,
Response::MODEL_ATTRIBUTE_EMAIL,
@@ -0,0 +1,66 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class ColumnBigInt extends Column
{
public function __construct()
{
parent::__construct();
$this
->addRule('key', [
'type' => self::TYPE_STRING,
'description' => 'Column Key.',
'default' => '',
'example' => 'count',
])
->addRule('type', [
'type' => self::TYPE_STRING,
'description' => 'Column type.',
'default' => '',
'example' => 'bigint',
])
->addRule('min', [
'type' => self::TYPE_INTEGER,
'format' => 'int64',
'description' => 'Minimum value to enforce for new documents.',
'default' => null,
'required' => false,
'example' => 1,
])
->addRule('max', [
'type' => self::TYPE_INTEGER,
'format' => 'int64',
'description' => 'Maximum value to enforce for new documents.',
'default' => null,
'required' => false,
'example' => 10,
])
->addRule('default', [
'type' => self::TYPE_INTEGER,
'format' => 'int64',
'description' => 'Default value for column when not provided. Cannot be set when column is required.',
'default' => null,
'required' => false,
'example' => 10,
])
;
}
public array $conditions = [
'type' => 'bigint'
];
public function getName(): string
{
return 'ColumnBigInt';
}
public function getType(): string
{
return Response::MODEL_COLUMN_BIGINT;
}
}
@@ -19,6 +19,9 @@ class ColumnList extends Model
->addRule('columns', [
'type' => [
Response::MODEL_COLUMN_BOOLEAN,
// BigInt must come before Integer: response model dispatch is "first match wins",
// and Integer matches all int types (including bigint), while BigInt is more specific (size=8).
Response::MODEL_COLUMN_BIGINT,
Response::MODEL_COLUMN_INTEGER,
Response::MODEL_COLUMN_FLOAT,
Response::MODEL_COLUMN_EMAIL,
@@ -42,13 +42,13 @@ class OAuth2Oidc extends OAuth2Base
'default' => '',
'example' => 'https://myoauth.com/oauth2/authorize',
])
->addRule('tokenUrl', [
->addRule('tokenURL', [
'type' => self::TYPE_STRING,
'description' => 'OpenID Connect token endpoint URL.',
'default' => '',
'example' => 'https://myoauth.com/oauth2/token',
])
->addRule('userInfoUrl', [
->addRule('userInfoURL', [
'type' => self::TYPE_STRING,
'description' => 'OpenID Connect user info endpoint URL.',
'default' => '',
+4 -4
View File
@@ -74,7 +74,7 @@ class Rule extends Model
])
->addRule('deploymentResourceId', [
'type' => self::TYPE_STRING,
'description' => 'ID deployment\'s resource. Used if type is "deployment"',
'description' => 'ID of deployment\'s resource (site or function ID). Used if type is "deployment"',
'default' => '',
'example' => 'n3u9feiwmf',
])
@@ -86,10 +86,10 @@ class Rule extends Model
])
->addRule('status', [
'type' => self::TYPE_ENUM,
'description' => 'Domain verification status. Possible values are "created", "verifying", "verified" and "unverified"',
'default' => 'created',
'description' => 'Domain verification status. Possible values are "unverified", "verifying", "verified"',
'default' => 'unverified',
'example' => 'verified',
'enum' => ['created', 'verifying', 'verified', 'unverified'],
'enum' => ['unverified', 'verifying', 'verified'],
])
->addRule('logs', [
'type' => self::TYPE_STRING,
@@ -63,6 +63,9 @@ class Table extends Model
->addRule('columns', [
'type' => [
Response::MODEL_COLUMN_BOOLEAN,
// BigInt must come before Integer: response model dispatch is "first match wins",
// and Integer matches all int types (including bigint), while BigInt is more specific (size=8).
Response::MODEL_COLUMN_BIGINT,
Response::MODEL_COLUMN_INTEGER,
Response::MODEL_COLUMN_FLOAT,
Response::MODEL_COLUMN_EMAIL,
+7
View File
@@ -0,0 +1,7 @@
<?php
namespace Executor;
class Exception extends \Exception
{
}
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace Executor\Exception;
use Executor\Exception;
use Throwable;
class Timeout extends Exception
{
public function __construct(
string $message,
private readonly int $timeoutSeconds,
int $code = 0,
?Throwable $previous = null,
) {
parent::__construct($message, $code, $previous);
}
public function getTimeoutSeconds(): int
{
return $this->timeoutSeconds;
}
}
+9 -9
View File
@@ -2,9 +2,9 @@
namespace Executor;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Utopia\Fetch\BodyMultipart;
use Exception;
use Executor\Exception as ExecutorException;
use Executor\Exception\Timeout as ExecutorTimeout;
use Utopia\System\System;
class Executor
@@ -104,7 +104,7 @@ class Executor
$status = $response['headers']['status-code'];
if ($status >= 400) {
$message = \is_string($response['body']) ? $response['body'] : $response['body']['message'];
throw new \Exception($message, $status);
throw new ExecutorException($message, $status);
}
return $response['body'];
@@ -163,7 +163,7 @@ class Executor
}
if ($status >= 400) {
throw new \Exception($message, $status);
throw new ExecutorException($message, $status);
}
return $response['body'];
@@ -247,7 +247,7 @@ class Executor
$status = $response['headers']['status-code'];
if ($status >= 400) {
$message = \is_string($response['body']) ? $response['body'] : $response['body']['message'];
throw new \Exception($message, $status);
throw new ExecutorException($message, $status);
}
$headers = $response['body']['headers'] ?? [];
@@ -281,7 +281,7 @@ class Executor
$status = $response['headers']['status-code'];
if ($status >= 400) {
$message = \is_string($response['body']) ? $response['body'] : $response['body']['message'];
throw new \Exception($message, $status);
throw new ExecutorException($message, $status);
}
return $response['body'];
@@ -401,7 +401,7 @@ class Executor
$json = json_decode($responseBody, true);
if ($json === null) {
throw new Exception('Failed to parse response: ' . $responseBody);
throw new ExecutorException('Failed to parse response: ' . $responseBody);
}
$responseBody = $json;
@@ -412,9 +412,9 @@ class Executor
if ($curlError) {
if ($curlError == CURLE_OPERATION_TIMEDOUT) {
throw new AppwriteException(AppwriteException::FUNCTION_SYNCHRONOUS_TIMEOUT);
throw new ExecutorTimeout('Executor request timed out', $timeout);
}
throw new Exception($curlErrorMessage . ' with status code ' . $responseStatus, $responseStatus);
throw new ExecutorException($curlErrorMessage . ' with status code ' . $responseStatus, $responseStatus);
}
$responseHeaders['status-code'] = $responseStatus;
+1
View File
@@ -380,6 +380,7 @@ function computeFlow(ctx) {
api('GET', '/functions/runtimes', null, ctx.sessionHeaders, [200], 'functions.runtimes.list');
api('GET', '/functions/specifications', null, ctx.apiHeaders, [200], 'functions.specifications.list');
const functionVariable = api('POST', `/functions/${functionId}/variables`, {
variableId: 'unique()',
key: 'BENCHMARK',
value: 'true',
secret: false,
+2 -2
View File
@@ -173,8 +173,8 @@ trait ProjectCustom
'oauth2.write',
'mocks.read',
'mocks.write',
'policies.read',
'policies.write',
'project.policies.read',
'project.policies.write',
'templates.read',
'templates.write',
],
@@ -4163,4 +4163,72 @@ class AccountCustomClientTest extends Scope
$this->assertEquals(401, $verification3['headers']['status-code']);
}
public function testRefreshEmailPasswordSession(): void
{
$email = uniqid() . 'user@localhost.test';
$account = $this->client->call(Client::METHOD_POST, '/account', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => ID::unique(),
'email' => $email,
'password' => 'password',
]);
$this->assertEquals(201, $account['headers']['status-code']);
$session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'email' => $email,
'password' => 'password',
]);
$this->assertEquals(201, $session['headers']['status-code']);
$this->assertNotEmpty($session['body']['$id']);
$sessionId = $session['body']['$id'];
$cookie = 'a_session_' . $this->getProject()['$id'] . '=' .$session['cookies']['a_session_' . $this->getProject()['$id']];
$session = $this->client->call(Client::METHOD_GET, '/account/sessions/current', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => $cookie,
]));
$this->assertEquals(200, $session['headers']['status-code']);
$this->assertNotEmpty($session['body']['expire']);
$expiryBefore = $session['body']['expire'];
\sleep(3); // Small delay to ensure expiry an expand
$session = $this->client->call(Client::METHOD_PATCH, '/account/sessions/' . $sessionId, array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => $cookie,
]));
$this->assertEquals(200, $session['headers']['status-code']);
$this->assertNotEmpty($session['body']['expire']);
$expiryAfter = $session['body']['expire'];
$this->assertGreaterThan(\strtotime($expiryBefore), \strtotime($expiryAfter));
$session = $this->client->call(Client::METHOD_GET, '/account/sessions/current', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => $cookie,
]));
$this->assertEquals(200, $session['headers']['status-code']);
$this->assertEquals(\strtotime($expiryAfter), \strtotime($session['body']['expire']));
}
}
@@ -7,8 +7,10 @@ use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideConsole;
use Utopia\Console;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
class FunctionsConsoleClientTest extends Scope
{
@@ -70,6 +72,7 @@ class FunctionsConsoleClientTest extends Scope
$variable = $this->createVariable(
$functionId,
[
'variableId' => ID::unique(),
'key' => 'APP_TEST',
'value' => 'TESTINGVALUE',
'secret' => false
@@ -82,6 +85,7 @@ class FunctionsConsoleClientTest extends Scope
$secretVariable = $this->createVariable(
$functionId,
[
'variableId' => ID::unique(),
'key' => 'APP_TEST_1',
'value' => 'TESTINGVALUE_1',
'secret' => true
@@ -196,6 +200,7 @@ class FunctionsConsoleClientTest extends Scope
$variable = $this->createVariable(
$functionId,
[
'variableId' => ID::unique(),
'key' => 'APP_TEST',
'value' => 'TESTINGVALUE',
'secret' => false
@@ -208,6 +213,7 @@ class FunctionsConsoleClientTest extends Scope
$variable = $this->createVariable(
$functionId,
[
'variableId' => ID::unique(),
'key' => 'APP_TEST_1',
'value' => 'TESTINGVALUE_1',
'secret' => true
@@ -226,6 +232,7 @@ class FunctionsConsoleClientTest extends Scope
$variable = $this->createVariable(
$functionId,
[
'variableId' => ID::unique(),
'key' => 'APP_TEST',
'value' => 'ANOTHERTESTINGVALUE',
'secret' => false
@@ -234,10 +241,47 @@ class FunctionsConsoleClientTest extends Scope
$this->assertEquals(409, $variable['headers']['status-code']);
// Test for invalid variableId
$variable = $this->createVariable(
$functionId,
[
'variableId' => '!invalid-id!',
'key' => 'INVALID_ID_KEY',
'value' => 'value',
]
);
$this->assertEquals(400, $variable['headers']['status-code']);
// Test for duplicate variableId
$duplicateVariableId = ID::unique();
$variable = $this->createVariable(
$functionId,
[
'variableId' => $duplicateVariableId,
'key' => 'DUP_ID_KEY_1',
'value' => 'value1',
]
);
$this->assertEquals(201, $variable['headers']['status-code']);
$duplicate = $this->createVariable(
$functionId,
[
'variableId' => $duplicateVariableId,
'key' => 'DUP_ID_KEY_2',
'value' => 'value2',
]
);
$this->assertEquals(409, $duplicate['headers']['status-code']);
// Test for invalid key
$variable = $this->createVariable(
$functionId,
[
'variableId' => ID::unique(),
'key' => str_repeat("A", 256),
'value' => 'TESTINGVALUE'
]
@@ -249,6 +293,7 @@ class FunctionsConsoleClientTest extends Scope
$variable = $this->createVariable(
$functionId,
[
'variableId' => ID::unique(),
'key' => 'LONGKEY',
'value' => str_repeat("#", 8193),
]
@@ -283,6 +328,150 @@ class FunctionsConsoleClientTest extends Scope
*/
}
public function testListVariablesWithLimit(): void
{
// Create a fresh function for this test
$function = $this->createFunction([
'functionId' => ID::unique(),
'name' => 'Test List Variables With Limit',
'execute' => [Role::user($this->getUser()['$id'])->toString()],
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 10,
]);
$this->assertEquals(201, $function['headers']['status-code']);
$functionId = $function['body']['$id'];
$variable1 = $this->createVariable($functionId, [
'variableId' => ID::unique(),
'key' => 'LIMIT_KEY_1',
'value' => 'limit-value-1',
]);
$this->assertEquals(201, $variable1['headers']['status-code']);
$variable2 = $this->createVariable($functionId, [
'variableId' => ID::unique(),
'key' => 'LIMIT_KEY_2',
'value' => 'limit-value-2',
]);
$this->assertEquals(201, $variable2['headers']['status-code']);
// List with limit of 1
$response = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/variables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::limit(1)->toString(),
],
'total' => true,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(1, $response['body']['variables']);
$this->assertGreaterThanOrEqual(2, $response['body']['total']);
$this->cleanupFunction($functionId);
}
public function testListVariablesWithoutTotal(): void
{
// Create a fresh function for this test
$function = $this->createFunction([
'functionId' => ID::unique(),
'name' => 'Test List Variables Without Total',
'execute' => [Role::user($this->getUser()['$id'])->toString()],
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 10,
]);
$this->assertEquals(201, $function['headers']['status-code']);
$functionId = $function['body']['$id'];
$variable = $this->createVariable($functionId, [
'variableId' => ID::unique(),
'key' => 'NO_TOTAL_KEY',
'value' => 'no-total-value',
]);
$this->assertEquals(201, $variable['headers']['status-code']);
// List with total=false
$response = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/variables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'total' => false,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(0, $response['body']['total']);
$this->assertGreaterThanOrEqual(1, \count($response['body']['variables']));
$this->cleanupFunction($functionId);
}
public function testListVariablesCursorPagination(): void
{
// Create a fresh function for this test
$function = $this->createFunction([
'functionId' => ID::unique(),
'name' => 'Test List Variables Cursor Pagination',
'execute' => [Role::user($this->getUser()['$id'])->toString()],
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 10,
]);
$this->assertEquals(201, $function['headers']['status-code']);
$functionId = $function['body']['$id'];
$variable1 = $this->createVariable($functionId, [
'variableId' => ID::unique(),
'key' => 'CURSOR_KEY_1',
'value' => 'cursor-value-1',
]);
$this->assertEquals(201, $variable1['headers']['status-code']);
$variable2 = $this->createVariable($functionId, [
'variableId' => ID::unique(),
'key' => 'CURSOR_KEY_2',
'value' => 'cursor-value-2',
]);
$this->assertEquals(201, $variable2['headers']['status-code']);
// Get first page with limit 1
$page1 = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/variables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::limit(1)->toString(),
],
'total' => true,
]);
$this->assertEquals(200, $page1['headers']['status-code']);
$this->assertCount(1, $page1['body']['variables']);
$cursorId = $page1['body']['variables'][0]['$id'];
// Get next page using cursor
$page2 = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/variables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::limit(1)->toString(),
Query::cursorAfter(new Document(['$id' => $cursorId]))->toString(),
],
'total' => true,
]);
$this->assertEquals(200, $page2['headers']['status-code']);
$this->assertCount(1, $page2['body']['variables']);
$this->assertNotEquals($cursorId, $page2['body']['variables'][0]['$id']);
$this->cleanupFunction($functionId);
}
public function testGetVariable(): void
{
$data = $this->setupTestVariables();
@@ -337,6 +526,7 @@ class FunctionsConsoleClientTest extends Scope
$functionId = $function['body']['$id'];
$variable = $this->createVariable($functionId, [
'variableId' => ID::unique(),
'key' => 'APP_TEST',
'value' => 'TESTINGVALUE',
'secret' => false
@@ -345,6 +535,7 @@ class FunctionsConsoleClientTest extends Scope
$variableId = $variable['body']['$id'];
$secretVariable = $this->createVariable($functionId, [
'variableId' => ID::unique(),
'key' => 'APP_TEST_1',
'value' => 'TESTINGVALUE_1',
'secret' => true
@@ -457,6 +648,7 @@ class FunctionsConsoleClientTest extends Scope
* Test for FAILURE
*/
// Update with no parameters should fail with 400
$response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@@ -464,6 +656,7 @@ class FunctionsConsoleClientTest extends Scope
$this->assertEquals(400, $response['headers']['status-code']);
// Update with only value should succeed
$response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@@ -471,7 +664,7 @@ class FunctionsConsoleClientTest extends Scope
'value' => 'TESTINGVALUEUPDATED_2'
]);
$this->assertEquals(400, $response['headers']['status-code']);
$this->assertEquals(200, $response['headers']['status-code']);
$longKey = str_repeat("A", 256);
$response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([
@@ -496,6 +689,110 @@ class FunctionsConsoleClientTest extends Scope
$this->assertEquals(400, $response['headers']['status-code']);
}
public function testUpdateVariableKey(): void
{
// Create a fresh function and variable for this test
$function = $this->createFunction([
'functionId' => ID::unique(),
'name' => 'Test Update Variable Key',
'execute' => [Role::user($this->getUser()['$id'])->toString()],
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 10,
]);
$this->assertEquals(201, $function['headers']['status-code']);
$functionId = $function['body']['$id'];
$variable = $this->createVariable($functionId, [
'variableId' => ID::unique(),
'key' => 'KEY_BEFORE',
'value' => 'unchanged-value',
'secret' => false
]);
$this->assertEquals(201, $variable['headers']['status-code']);
$variableId = $variable['body']['$id'];
// Update only key (key is nullable, but we provide a new key)
$response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'key' => 'KEY_AFTER',
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('KEY_AFTER', $response['body']['key']);
$this->assertEquals('unchanged-value', $response['body']['value']);
$this->cleanupFunction($functionId);
}
public function testUpdateVariableValueOnly(): void
{
// Create a fresh function and variable for this test
$function = $this->createFunction([
'functionId' => ID::unique(),
'name' => 'Test Update Variable Value',
'execute' => [Role::user($this->getUser()['$id'])->toString()],
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 10,
]);
$this->assertEquals(201, $function['headers']['status-code']);
$functionId = $function['body']['$id'];
$variable = $this->createVariable($functionId, [
'variableId' => ID::unique(),
'key' => 'UNCHANGED_KEY',
'value' => 'value-before',
'secret' => false
]);
$this->assertEquals(201, $variable['headers']['status-code']);
$variableId = $variable['body']['$id'];
// Update only value
$response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/' . $variableId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'value' => 'value-after',
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('UNCHANGED_KEY', $response['body']['key']);
$this->assertEquals('value-after', $response['body']['value']);
$this->cleanupFunction($functionId);
}
public function testUpdateVariableNotFound(): void
{
// Create a fresh function for this test
$function = $this->createFunction([
'functionId' => ID::unique(),
'name' => 'Test Update Variable Not Found',
'execute' => [Role::user($this->getUser()['$id'])->toString()],
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 10,
]);
$this->assertEquals(201, $function['headers']['status-code']);
$functionId = $function['body']['$id'];
$response = $this->client->call(Client::METHOD_PUT, '/functions/' . $functionId . '/variables/non-existent-id', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'key' => 'NEW_KEY',
'value' => 'new-value',
]);
$this->assertEquals(404, $response['headers']['status-code']);
$this->assertEquals('variable_not_found', $response['body']['type']);
$this->cleanupFunction($functionId);
}
public function testDeleteVariable(): void
{
// Create a fresh function and variables for this test since it deletes them
@@ -512,6 +809,7 @@ class FunctionsConsoleClientTest extends Scope
$functionId = $function['body']['$id'];
$variable = $this->createVariable($functionId, [
'variableId' => ID::unique(),
'key' => 'APP_TEST',
'value' => 'TESTINGVALUE',
'secret' => false
@@ -520,6 +818,7 @@ class FunctionsConsoleClientTest extends Scope
$variableId = $variable['body']['$id'];
$secretVariable = $this->createVariable($functionId, [
'variableId' => ID::unique(),
'key' => 'APP_TEST_1',
'value' => 'TESTINGVALUE_1',
'secret' => true
@@ -585,6 +884,7 @@ class FunctionsConsoleClientTest extends Scope
// create variable
$variable = $this->createVariable($functionId, [
'variableId' => ID::unique(),
'key' => 'CUSTOM_VARIABLE',
'value' => 'a_secret_value',
'secret' => true,
@@ -53,14 +53,17 @@ class FunctionsCustomServerTest extends Scope
$functionId = $function['body']['$id'] ?? '';
$variable = $this->createVariable($functionId, [
'variableId' => 'unique()',
'key' => 'funcKey1',
'value' => 'funcValue1',
]);
$variable2 = $this->createVariable($functionId, [
'variableId' => 'unique()',
'key' => 'funcKey2',
'value' => 'funcValue2',
]);
$variable3 = $this->createVariable($functionId, [
'variableId' => 'unique()',
'key' => 'funcKey3',
'value' => 'funcValue3',
]);
@@ -109,6 +112,7 @@ class FunctionsCustomServerTest extends Scope
// Create a variable for later tests
$variable = $this->createVariable($functionId, [
'variableId' => 'unique()',
'key' => 'GLOBAL_VARIABLE',
'value' => 'Global Variable Value',
]);
@@ -278,14 +282,17 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals(10, $function['body']['timeout']);
$variable = $this->createVariable($functionId, [
'variableId' => 'unique()',
'key' => 'funcKey1',
'value' => 'funcValue1',
]);
$variable2 = $this->createVariable($functionId, [
'variableId' => 'unique()',
'key' => 'funcKey2',
'value' => 'funcValue2',
]);
$variable3 = $this->createVariable($functionId, [
'variableId' => 'unique()',
'key' => 'funcKey3',
'value' => 'funcValue3',
]);
@@ -521,6 +528,7 @@ class FunctionsCustomServerTest extends Scope
// Create a variable for later tests
$variable = $this->createVariable($functionId, [
'variableId' => 'unique()',
'key' => 'GLOBAL_VARIABLE',
'value' => 'Global Variable Value',
]);
@@ -2011,6 +2019,7 @@ class FunctionsCustomServerTest extends Scope
]);
$variable = $this->createVariable($functionId, [
'variableId' => 'unique()',
'key' => 'CUSTOM_VARIABLE',
'value' => 'variable'
]);
@@ -55,10 +55,10 @@ class FunctionsClientTest extends Scope
$query = '
mutation createVariables($functionId: String!) {
var1: functionsCreateVariable(functionId: $functionId, key: "name", value: "John Doe") {
var1: functionsCreateVariable(functionId: $functionId, variableId: "unique()", key: "name", value: "John Doe") {
_id
}
var2: functionsCreateVariable(functionId: $functionId, key: "age", value: "42") {
var2: functionsCreateVariable(functionId: $functionId, variableId: "unique()", key: "age", value: "42") {
_id
}
}
@@ -55,10 +55,10 @@ class FunctionsServerTest extends Scope
$query = '
mutation createVariables($functionId: String!) {
var1: functionsCreateVariable(functionId: $functionId, key: "name", value: "John Doe") {
var1: functionsCreateVariable(functionId: $functionId, variableId: "unique()", key: "name", value: "John Doe") {
_id
}
var2: functionsCreateVariable(functionId: $functionId, key: "age", value: "42") {
var2: functionsCreateVariable(functionId: $functionId, variableId: "unique()", key: "age", value: "42") {
_id
}
}
File diff suppressed because it is too large Load Diff
+217 -55
View File
@@ -5,6 +5,7 @@ namespace Tests\E2E\Services\Project;
use PHPUnit\Framework\Attributes\Before;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\E2E\Client;
use Utopia\Database\Query;
trait OAuth2Base
{
@@ -172,6 +173,40 @@ trait OAuth2Base
$this->assertNotContains('mock-unverified', $ids);
}
public function testListOAuth2ProvidersTotalFalse(): void
{
$response = $this->listOAuth2Providers(total: false);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertSame(0, $response['body']['total']);
$this->assertGreaterThan(0, \count($response['body']['providers']));
}
public function testListOAuth2ProvidersWithLimit(): void
{
$response = $this->listOAuth2Providers([
Query::limit(1)->toString(),
]);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertCount(1, $response['body']['providers']);
$this->assertGreaterThan(1, $response['body']['total']);
}
public function testListOAuth2ProvidersWithOffset(): void
{
$listAll = $this->listOAuth2Providers();
$this->assertSame(200, $listAll['headers']['status-code']);
$listOffset = $this->listOAuth2Providers([
Query::offset(1)->toString(),
]);
$this->assertSame(200, $listOffset['headers']['status-code']);
$this->assertCount(\count($listAll['body']['providers']) - 1, $listOffset['body']['providers']);
$this->assertSame($listAll['body']['total'], $listOffset['body']['total']);
}
// =========================================================================
// Get OAuth2 provider
// =========================================================================
@@ -188,6 +223,28 @@ trait OAuth2Base
$this->assertSame('', $response['body']['clientSecret']);
}
public function testGetOAuth2ProviderWithAlias(): void
{
// The action declares the canonical param name as `providerId` and
// registers `provider` as an alias so that older SDK versions that
// send the provider in the query string continue to work.
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
];
$headers = \array_merge($headers, $this->getHeaders());
// Call with `provider` in query string (legacy behaviour)
$response = $this->client->call(
Client::METHOD_GET,
'/project/oauth2/github?provider=github',
$headers,
);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertSame('github', $response['body']['$id']);
}
public function testGetOAuth2ProviderClientSecretWriteOnly(): void
{
$this->updateOAuth2('amazon', [
@@ -221,19 +278,23 @@ trait OAuth2Base
public function testGetOAuth2ProviderUnsupported(): void
{
// The `providerId` param is validated by a WhiteList of registered
// OAuth2 provider keys, so an unknown value is rejected at validation
// time — before the action runs — and surfaces as a generic argument
// error rather than `project_provider_unsupported`.
$response = $this->getOAuth2Provider('not-a-real-provider');
$this->assertSame(400, $response['headers']['status-code']);
$this->assertSame('project_provider_unsupported', $response['body']['type']);
$this->assertSame('general_argument_invalid', $response['body']['type']);
}
public function testGetOAuth2ProviderRegisteredInConfigButNoUpdateClass(): void
{
// `mock` is present in oAuthProviders config (enabled: true) but is NOT
// registered in Base::getProviderActions(). Get::action has two
// separate `unsupported` throw branches — testGetOAuth2ProviderUnsupported
// covers the first (provider missing from config); this covers the
// second (provider in config but missing from the action registry).
// `mock` is present in oAuthProviders config (enabled: true) but is
// NOT registered in Base::getProviderActions(). It passes the
// WhiteList validator (which only checks config membership) and
// reaches the action body, where the action-registry check throws
// `project_provider_unsupported`.
$response = $this->getOAuth2Provider('mock');
$this->assertSame(400, $response['headers']['status-code']);
@@ -1590,8 +1651,8 @@ trait OAuth2Base
$this->assertSame(200, $response['headers']['status-code']);
$this->assertSame('https://idp.example.com/.well-known/openid-configuration', $response['body']['wellKnownURL']);
$this->assertArrayHasKey('authorizationURL', $response['body']);
$this->assertArrayHasKey('tokenUrl', $response['body']);
$this->assertArrayHasKey('userInfoUrl', $response['body']);
$this->assertArrayHasKey('tokenURL', $response['body']);
$this->assertArrayHasKey('userInfoURL', $response['body']);
// Cleanup
$this->updateOAuth2('oidc', [
@@ -1599,8 +1660,8 @@ trait OAuth2Base
'clientSecret' => '',
'wellKnownURL' => '',
'authorizationURL' => '',
'tokenUrl' => '',
'userInfoUrl' => '',
'tokenURL' => '',
'userInfoURL' => '',
'enabled' => false,
]);
}
@@ -1611,15 +1672,15 @@ trait OAuth2Base
'clientId' => 'oidc-discovery',
'clientSecret' => 'oidc-discovery-secret',
'authorizationURL' => 'https://idp.example.com/oauth2/authorize',
'tokenUrl' => 'https://idp.example.com/oauth2/token',
'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo',
'tokenURL' => 'https://idp.example.com/oauth2/token',
'userInfoURL' => 'https://idp.example.com/oauth2/userinfo',
'enabled' => false,
]);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertSame('https://idp.example.com/oauth2/authorize', $response['body']['authorizationURL']);
$this->assertSame('https://idp.example.com/oauth2/token', $response['body']['tokenUrl']);
$this->assertSame('https://idp.example.com/oauth2/userinfo', $response['body']['userInfoUrl']);
$this->assertSame('https://idp.example.com/oauth2/token', $response['body']['tokenURL']);
$this->assertSame('https://idp.example.com/oauth2/userinfo', $response['body']['userInfoURL']);
// Cleanup
$this->updateOAuth2('oidc', [
@@ -1627,8 +1688,8 @@ trait OAuth2Base
'clientSecret' => '',
'wellKnownURL' => '',
'authorizationURL' => '',
'tokenUrl' => '',
'userInfoUrl' => '',
'tokenURL' => '',
'userInfoURL' => '',
'enabled' => false,
]);
}
@@ -1640,8 +1701,8 @@ trait OAuth2Base
'clientSecret' => '',
'wellKnownURL' => '',
'authorizationURL' => '',
'tokenUrl' => '',
'userInfoUrl' => '',
'tokenURL' => '',
'userInfoURL' => '',
'enabled' => false,
]);
@@ -1670,8 +1731,8 @@ trait OAuth2Base
'clientSecret' => '',
'wellKnownURL' => '',
'authorizationURL' => '',
'tokenUrl' => '',
'userInfoUrl' => '',
'tokenURL' => '',
'userInfoURL' => '',
'enabled' => false,
]);
@@ -1679,7 +1740,7 @@ trait OAuth2Base
'clientId' => 'oidc-partial',
'clientSecret' => 'oidc-partial-secret',
'authorizationURL' => 'https://idp.example.com/oauth2/authorize',
'tokenUrl' => 'https://idp.example.com/oauth2/token',
'tokenURL' => 'https://idp.example.com/oauth2/token',
'enabled' => true,
]);
@@ -1692,8 +1753,8 @@ trait OAuth2Base
'clientSecret' => '',
'wellKnownURL' => '',
'authorizationURL' => '',
'tokenUrl' => '',
'userInfoUrl' => '',
'tokenURL' => '',
'userInfoURL' => '',
'enabled' => false,
]);
}
@@ -1724,8 +1785,8 @@ trait OAuth2Base
'clientSecret' => '',
'wellKnownURL' => '',
'authorizationURL' => '',
'tokenUrl' => '',
'userInfoUrl' => '',
'tokenURL' => '',
'userInfoURL' => '',
'enabled' => false,
]);
}
@@ -1755,8 +1816,8 @@ trait OAuth2Base
'clientSecret' => '',
'wellKnownURL' => '',
'authorizationURL' => '',
'tokenUrl' => '',
'userInfoUrl' => '',
'tokenURL' => '',
'userInfoURL' => '',
'enabled' => false,
]);
}
@@ -1770,8 +1831,8 @@ trait OAuth2Base
'clientSecret' => '',
'wellKnownURL' => '',
'authorizationURL' => '',
'tokenUrl' => '',
'userInfoUrl' => '',
'tokenURL' => '',
'userInfoURL' => '',
'enabled' => false,
]);
@@ -1780,7 +1841,7 @@ trait OAuth2Base
'clientId' => 'oidc-split-discovery',
'clientSecret' => 'oidc-split-discovery-secret',
'authorizationURL' => 'https://idp.example.com/oauth2/authorize',
'tokenUrl' => 'https://idp.example.com/oauth2/token',
'tokenURL' => 'https://idp.example.com/oauth2/token',
'enabled' => false,
]);
@@ -1788,19 +1849,19 @@ trait OAuth2Base
// state must include the two stored URLs + the new one to satisfy
// the all-three-discovery-URLs branch of the enable check.
$enable = $this->updateOAuth2('oidc', [
'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo',
'userInfoURL' => 'https://idp.example.com/oauth2/userinfo',
'enabled' => true,
]);
$this->assertSame(200, $enable['headers']['status-code']);
$this->assertTrue($enable['body']['enabled']);
// Confirm all three URLs ended up persisted (merge wrote the new
// userInfoUrl while preserving the previously stored two).
// userInfoURL while preserving the previously stored two).
$get = $this->getOAuth2Provider('oidc');
$this->assertSame(200, $get['headers']['status-code']);
$this->assertSame('https://idp.example.com/oauth2/authorize', $get['body']['authorizationURL']);
$this->assertSame('https://idp.example.com/oauth2/token', $get['body']['tokenUrl']);
$this->assertSame('https://idp.example.com/oauth2/userinfo', $get['body']['userInfoUrl']);
$this->assertSame('https://idp.example.com/oauth2/token', $get['body']['tokenURL']);
$this->assertSame('https://idp.example.com/oauth2/userinfo', $get['body']['userInfoURL']);
// Cleanup
$this->updateOAuth2('oidc', [
@@ -1808,8 +1869,8 @@ trait OAuth2Base
'clientSecret' => '',
'wellKnownURL' => '',
'authorizationURL' => '',
'tokenUrl' => '',
'userInfoUrl' => '',
'tokenURL' => '',
'userInfoURL' => '',
'enabled' => false,
]);
}
@@ -1822,8 +1883,8 @@ trait OAuth2Base
'clientSecret' => 'oidc-clear-then-enable-secret',
'wellKnownURL' => 'https://idp.example.com/.well-known/openid-configuration',
'authorizationURL' => '',
'tokenUrl' => '',
'userInfoUrl' => '',
'tokenURL' => '',
'userInfoURL' => '',
'enabled' => false,
]);
@@ -1846,8 +1907,8 @@ trait OAuth2Base
'clientSecret' => '',
'wellKnownURL' => '',
'authorizationURL' => '',
'tokenUrl' => '',
'userInfoUrl' => '',
'tokenURL' => '',
'userInfoURL' => '',
'enabled' => false,
]);
}
@@ -1868,16 +1929,16 @@ trait OAuth2Base
$switch = $this->updateOAuth2('oidc', [
'wellKnownURL' => '',
'authorizationURL' => 'https://idp.example.com/oauth2/authorize',
'tokenUrl' => 'https://idp.example.com/oauth2/token',
'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo',
'tokenURL' => 'https://idp.example.com/oauth2/token',
'userInfoURL' => 'https://idp.example.com/oauth2/userinfo',
'enabled' => true,
]);
$this->assertSame(200, $switch['headers']['status-code']);
$this->assertTrue($switch['body']['enabled']);
$this->assertSame('', $switch['body']['wellKnownURL']);
$this->assertSame('https://idp.example.com/oauth2/authorize', $switch['body']['authorizationURL']);
$this->assertSame('https://idp.example.com/oauth2/token', $switch['body']['tokenUrl']);
$this->assertSame('https://idp.example.com/oauth2/userinfo', $switch['body']['userInfoUrl']);
$this->assertSame('https://idp.example.com/oauth2/token', $switch['body']['tokenURL']);
$this->assertSame('https://idp.example.com/oauth2/userinfo', $switch['body']['userInfoURL']);
// Cleanup
$this->updateOAuth2('oidc', [
@@ -1885,8 +1946,8 @@ trait OAuth2Base
'clientSecret' => '',
'wellKnownURL' => '',
'authorizationURL' => '',
'tokenUrl' => '',
'userInfoUrl' => '',
'tokenURL' => '',
'userInfoURL' => '',
'enabled' => false,
]);
}
@@ -1900,23 +1961,23 @@ trait OAuth2Base
'clientSecret' => 'oidc-clear-secret',
'wellKnownURL' => 'https://idp.example.com/.well-known/openid-configuration',
'authorizationURL' => 'https://idp.example.com/oauth2/authorize',
'tokenUrl' => 'https://idp.example.com/oauth2/token',
'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo',
'tokenURL' => 'https://idp.example.com/oauth2/token',
'userInfoURL' => 'https://idp.example.com/oauth2/userinfo',
'enabled' => false,
]);
$response = $this->updateOAuth2('oidc', [
'wellKnownURL' => '',
'authorizationURL' => '',
'tokenUrl' => '',
'userInfoUrl' => '',
'tokenURL' => '',
'userInfoURL' => '',
]);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertSame('', $response['body']['wellKnownURL']);
$this->assertSame('', $response['body']['authorizationURL']);
$this->assertSame('', $response['body']['tokenUrl']);
$this->assertSame('', $response['body']['userInfoUrl']);
$this->assertSame('', $response['body']['tokenURL']);
$this->assertSame('', $response['body']['userInfoURL']);
// Cleanup
$this->updateOAuth2('oidc', [
@@ -1926,6 +1987,96 @@ trait OAuth2Base
]);
}
public function testUpdateOAuth2OidcBackwardCompatibleResponseFormat(): void
{
// Reset to clean state
$this->updateOAuth2('oidc', [
'clientId' => '',
'clientSecret' => '',
'wellKnownURL' => '',
'authorizationURL' => '',
'tokenURL' => '',
'userInfoURL' => '',
'enabled' => false,
]);
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.3',
];
$headers = \array_merge($headers, $this->getHeaders());
// Update using OLD param names (aliases must still work)
$response = $this->client->call(
Client::METHOD_PATCH,
'/project/oauth2/oidc',
$headers,
[
'clientId' => 'oidc-compat-client',
'clientSecret' => 'oidc-compat-secret',
'tokenUrl' => 'https://idp.example.com/oauth2/token',
'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo',
'enabled' => false,
],
);
$this->assertSame(200, $response['headers']['status-code']);
$this->assertArrayHasKey('tokenUrl', $response['body']);
$this->assertArrayHasKey('userInfoUrl', $response['body']);
$this->assertArrayNotHasKey('tokenURL', $response['body']);
$this->assertArrayNotHasKey('userInfoURL', $response['body']);
$this->assertSame('https://idp.example.com/oauth2/token', $response['body']['tokenUrl']);
$this->assertSame('https://idp.example.com/oauth2/userinfo', $response['body']['userInfoUrl']);
// GET with 1.9.3 format must also return old param names
$get = $this->client->call(
Client::METHOD_GET,
'/project/oauth2/oidc',
$headers,
);
$this->assertSame(200, $get['headers']['status-code']);
$this->assertArrayHasKey('tokenUrl', $get['body']);
$this->assertArrayHasKey('userInfoUrl', $get['body']);
$this->assertArrayNotHasKey('tokenURL', $get['body']);
$this->assertArrayNotHasKey('userInfoURL', $get['body']);
$this->assertSame('https://idp.example.com/oauth2/token', $get['body']['tokenUrl']);
$this->assertSame('https://idp.example.com/oauth2/userinfo', $get['body']['userInfoUrl']);
// LIST with 1.9.3 format must also return old param names for OIDC
$list = $this->client->call(
Client::METHOD_GET,
'/project/oauth2',
$headers,
);
$this->assertSame(200, $list['headers']['status-code']);
$oidcEntry = null;
foreach ($list['body']['providers'] as $provider) {
if ($provider['$id'] === 'oidc') {
$oidcEntry = $provider;
break;
}
}
$this->assertNotNull($oidcEntry, 'OIDC provider missing from listOAuth2Providers response');
$this->assertArrayHasKey('tokenUrl', $oidcEntry);
$this->assertArrayHasKey('userInfoUrl', $oidcEntry);
$this->assertArrayNotHasKey('tokenURL', $oidcEntry);
$this->assertArrayNotHasKey('userInfoURL', $oidcEntry);
$this->assertSame('https://idp.example.com/oauth2/token', $oidcEntry['tokenUrl']);
$this->assertSame('https://idp.example.com/oauth2/userinfo', $oidcEntry['userInfoUrl']);
// Cleanup
$this->updateOAuth2('oidc', [
'clientId' => '',
'clientSecret' => '',
'tokenURL' => '',
'userInfoURL' => '',
'enabled' => false,
]);
}
// =========================================================================
// Update Okta (clientId + clientSecret + optional domain/authServer)
// =========================================================================
@@ -2573,7 +2724,7 @@ trait OAuth2Base
);
}
protected function getOAuth2Provider(string $provider, bool $authenticated = true): mixed
protected function getOAuth2Provider(string $providerId, bool $authenticated = true): mixed
{
$headers = [
'content-type' => 'application/json',
@@ -2586,13 +2737,23 @@ trait OAuth2Base
return $this->client->call(
Client::METHOD_GET,
'/project/oauth2/' . $provider,
'/project/oauth2/' . $providerId,
$headers,
);
}
protected function listOAuth2Providers(bool $authenticated = true): mixed
protected function listOAuth2Providers(?array $queries = null, ?bool $total = null, bool $authenticated = true): mixed
{
$params = [];
if ($queries !== null) {
$params['queries'] = $queries;
}
if ($total !== null) {
$params['total'] = $total;
}
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@@ -2606,6 +2767,7 @@ trait OAuth2Base
Client::METHOD_GET,
'/project/oauth2',
$headers,
$params,
);
}
}
@@ -89,6 +89,7 @@ class WebhooksCustomServerTest extends Scope
$this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/variables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.3',
], $this->getHeaders()), [
'key' => 'key1',
'value' => 'value1',
@@ -699,6 +700,7 @@ class WebhooksCustomServerTest extends Scope
$variable = $this->client->call(Client::METHOD_POST, '/functions/' . $id . '/variables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-response-format' => '1.9.3',
], $this->getHeaders()), [
'key' => 'key1',
'value' => 'value1',
@@ -6813,6 +6813,7 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-mode' => 'admin',
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $token,
], [
'variableId' => $variableId,
'key' => 'APP_TEST_' . $variableId,
'value' => 'TESTINGVALUE',
'secret' => false
@@ -6832,6 +6833,7 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-mode' => 'admin',
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $token,
], [
'variableId' => $variableId,
'key' => 'APP_TEST_' . $variableId,
'value' => 'TESTINGVALUE',
'secret' => false
+689 -234
View File
@@ -2,298 +2,753 @@
namespace Tests\E2E\Services\Proxy;
use Appwrite\ID;
use Appwrite\Tests\Async;
use CURLFile;
use Tests\E2E\Client;
use Utopia\Console;
use Utopia\Database\Query;
use Utopia\System\System;
trait ProxyBase
{
use Async;
use ProxyHelpers;
protected function listRules(array $params = []): mixed
protected function tearDown(): void
{
$rule = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), $params);
return $rule;
}
protected function createAPIRule(string $domain): mixed
{
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'domain' => $domain,
// Cleanup for testRuleVerification test
// Required as it uses static domain name
$rules = $this->listRules([
'queries' => [
Query::endsWith('domain', 'webapp.com')->toString(),
Query::limit(1000)->toString(),
]
]);
$this->assertEquals(200, $rules['headers']['status-code']);
foreach ($rules['body']['rules'] as $rule) {
$ruleId = $rule['$id'];
$response = $this->deleteRule($ruleId);
$this->assertEquals(204, $response['headers']['status-code']);
}
return $rule;
if ($rules['body']['total'] > 0) {
$rules = $this->listRules([
'queries' => [
Query::endsWith('domain', 'webapp.com')->toString(),
Query::limit(1)->toString()
]
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertEquals(0, count($rules['body']['rules']));
$this->assertEquals(0, $rules['body']['total']);
}
}
protected function updateRuleVerification(string $ruleId): mixed
{
$rule = $this->client->call(Client::METHOD_PATCH, '/proxy/rules/' . $ruleId . '/verification', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
return $rule;
}
protected function createSiteRule(string $domain, string $siteId, string $branch = ''): mixed
{
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/site', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'domain' => $domain,
'siteId' => $siteId,
'branch' => $branch,
]);
return $rule;
}
protected function getRule(string $ruleId): mixed
{
$rule = $this->client->call(Client::METHOD_GET, '/proxy/rules/' . $ruleId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
return $rule;
}
protected function createRedirectRule(string $domain, string $url, int $statusCode, string $resourceType, string $resourceId): mixed
{
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/redirect', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'domain' => $domain,
'url' => $url,
'statusCode' => $statusCode,
'resourceType' => $resourceType,
'resourceId' => $resourceId,
]);
return $rule;
}
protected function createFunctionRule(string $domain, string $functionId, string $branch = ''): mixed
{
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/function', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'domain' => $domain,
'functionId' => $functionId,
'branch' => $branch,
]);
return $rule;
}
protected function deleteRule(string $ruleId): mixed
{
$rule = $this->client->call(Client::METHOD_DELETE, '/proxy/rules/' . $ruleId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
return $rule;
}
protected function setupAPIRule(string $domain): string
public function testCreateRule(): void
{
$domain = \uniqid() . '-api.myapp.com';
$rule = $this->createAPIRule($domain);
$this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule));
$this->assertEquals(201, $rule['headers']['status-code']);
$this->assertEquals($domain, $rule['body']['domain']);
$this->assertEquals('manual', $rule['body']['trigger']);
$this->assertArrayHasKey('$id', $rule['body']);
$this->assertArrayHasKey('domain', $rule['body']);
$this->assertArrayHasKey('type', $rule['body']);
$this->assertArrayHasKey('redirectUrl', $rule['body']);
$this->assertArrayHasKey('redirectStatusCode', $rule['body']);
$this->assertArrayHasKey('deploymentResourceType', $rule['body']);
$this->assertArrayHasKey('deploymentId', $rule['body']);
$this->assertArrayHasKey('deploymentResourceId', $rule['body']);
$this->assertArrayHasKey('deploymentVcsProviderBranch', $rule['body']);
$this->assertArrayHasKey('logs', $rule['body']);
$this->assertArrayHasKey('renewAt', $rule['body']);
return $rule['body']['$id'];
}
$ruleId = $rule['body']['$id'];
protected function setupRedirectRule(string $domain, string $url, int $statusCode, string $resourceType, string $resourceId): string
{
$rule = $this->createRedirectRule($domain, $url, $statusCode, $resourceType, $resourceId);
$rule = $this->createAPIRule($domain);
$this->assertEquals(409, $rule['headers']['status-code']);
$this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule));
return $rule['body']['$id'];
}
protected function setupFunctionRule(string $domain, string $functionId, string $branch = ''): string
{
$rule = $this->createFunctionRule($domain, $functionId, $branch);
$this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule));
return $rule['body']['$id'];
}
protected function setupSiteRule(string $domain, string $siteId, string $branch = ''): string
{
$rule = $this->createSiteRule($domain, $siteId, $branch);
$this->assertEquals(201, $rule['headers']['status-code'], 'Failed to setup rule: ' . \json_encode($rule));
return $rule['body']['$id'];
}
protected function cleanupRule(string $ruleId): void
{
$rule = $this->deleteRule($ruleId);
$this->assertEquals(204, $rule['headers']['status-code'], 'Failed to cleanup rule: ' . \json_encode($rule));
$this->assertEquals(204, $rule['headers']['status-code']);
}
protected function cleanupSite(string $siteId): void
public function testCreateRuleSetup(): void
{
$site = $this->client->call(Client::METHOD_DELETE, '/sites/' . $siteId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(204, $site['headers']['status-code'], 'Failed to cleanup site: ' . \json_encode($site));
$ruleId = $this->setupAPIRule(\uniqid() . '-api2.myapp.com');
$this->cleanupRule($ruleId);
}
protected function cleanupFunction(string $functionId): void
public function testCreateRuleApex(): void
{
$function = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals(204, $function['headers']['status-code'], 'Failed to cleanup function: ' . \json_encode($function));
$domain = \uniqid() . '.com';
$rule = $this->createAPIRule($domain);
$this->assertEquals(201, $rule['headers']['status-code']);
$this->assertEquals('unverified', $rule['body']['status']);
}
protected function setupSite(): mixed
public function testCreateRuleVcs(): void
{
// Site
$site = $this->client->call(Client::METHOD_POST, '/sites', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]), [
'siteId' => ID::unique(),
'name' => 'Proxy site',
'framework' => 'other',
'adapter' => 'static',
'buildRuntime' => 'static-1',
'outputDirectory' => './',
'buildCommand' => '',
'installCommand' => '',
'fallbackFile' => '',
]);
$domain = \uniqid() . '-vcs.myapp.com';
$this->assertEquals($site['headers']['status-code'], 201, 'Setup site failed with status code: ' . $site['headers']['status-code'] . ' and response: ' . json_encode($site['body'], JSON_PRETTY_PRINT));
$setup = $this->setupSite();
$siteId = $setup['siteId'];
$deploymentId = $setup['deploymentId'];
$siteId = $site['body']['$id'];
$this->assertNotEmpty($siteId);
$this->assertNotEmpty($deploymentId);
// Deployment
$deployment = $this->client->call(Client::METHOD_POST, '/sites/' . $siteId . '/deployments', array_merge([
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]), [
'code' => $this->packageSite('static'),
'activate' => 'true'
]);
$rule = $this->createSiteRule('commit-' . $domain, $siteId);
$this->assertEquals(201, $rule['headers']['status-code']);
$this->cleanupRule($rule['body']['$id']);
$this->assertEquals($deployment['headers']['status-code'], 202, 'Setup deployment failed with status code: ' . $deployment['headers']['status-code'] . ' and response: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT));
$deploymentId = $deployment['body']['$id'] ?? '';
$rule = $this->createSiteRule('branch-' . $domain, $siteId);
$this->assertEquals(201, $rule['headers']['status-code']);
$this->cleanupRule($rule['body']['$id']);
$this->assertEventually(function () use ($siteId, $deploymentId) {
$site = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]));
$this->assertEquals($deploymentId, $site['body']['deploymentId'], 'Deployment is not activated, deployment: ' . json_encode($site['body'], JSON_PRETTY_PRINT));
}, 120000, 500);
$rule = $this->createSiteRule('anything-' . $domain, $siteId);
$this->assertEquals(201, $rule['headers']['status-code']);
$this->cleanupRule($rule['body']['$id']);
return ['siteId' => $siteId, 'deploymentId' => $deploymentId];
$sitesDomain = \explode(',', System::getEnv('_APP_DOMAIN_SITES', ''))[0];
$domain = \uniqid() . '-vcs.' . $sitesDomain;
$rule = $this->createSiteRule('commit-' . $domain, $siteId);
$this->assertEquals(400, $rule['headers']['status-code']);
$rule = $this->createSiteRule('branch-' . $domain, $siteId);
$this->assertEquals(400, $rule['headers']['status-code']);
$rule = $this->createSiteRule('subdomain.anything-' . $domain, $siteId);
$this->assertEquals(400, $rule['headers']['status-code']);
$rule = $this->createSiteRule('anything-' . $domain, $siteId);
$this->assertEquals(201, $rule['headers']['status-code']);
$this->cleanupRule($rule['body']['$id']);
}
protected function setupFunction(): mixed
public function testCreateAPIRule(): void
{
// Function
$function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]), [
'functionId' => ID::unique(),
'runtime' => 'node-22',
'name' => 'Proxy Function',
'entrypoint' => 'index.js',
'commands' => '',
'execute' => ['any']
$domain = \uniqid() . '-api.custom.localhost';
$proxyClient = new Client();
$proxyClient->setEndpoint('http://appwrite.test');
$proxyClient->addHeader('x-appwrite-hostname', $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/versions');
$this->assertEquals(401, $response['headers']['status-code']);
$ruleId = $this->setupAPIRule($domain);
$this->assertNotEmpty($ruleId);
$response = $proxyClient->call(Client::METHOD_GET, '/versions');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(APP_VERSION_STABLE, $response['body']['server']);
$this->cleanupRule($ruleId);
$rule = $this->createAPIRule('http://' . $domain);
$this->assertEquals(400, $rule['headers']['status-code']);
$rule = $this->createAPIRule('https://' . $domain);
$this->assertEquals(400, $rule['headers']['status-code']);
$rule = $this->createAPIRule('wss://' . $domain);
$this->assertEquals(400, $rule['headers']['status-code']);
$rule = $this->createAPIRule($domain . '/some-path');
$this->assertEquals(400, $rule['headers']['status-code']);
}
public function testCreateRedirectRule(): void
{
$domain = \uniqid() . '-redirect.custom.localhost';
$proxyClient = new Client();
$proxyClient->setEndpoint('http://appwrite.test');
$proxyClient->addHeader('x-appwrite-hostname', $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/todos/1');
$this->assertEquals(401, $response['headers']['status-code']);
$siteId = $this->setupSite()['siteId'];
$ruleId = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 301, 'site', $siteId);
$this->assertNotEmpty($ruleId);
$response = $proxyClient->call(Client::METHOD_GET, '/todos/1');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(1, $response['body']['id']);
$response = $proxyClient->call(Client::METHOD_GET, '/');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(1, $response['body']['id']);
$response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false);
$this->assertEquals(301, $response['headers']['status-code']);
$this->assertEquals('https://jsonplaceholder.typicode.com/todos/1', $response['headers']['location']);
$domain = \uniqid() . '-redirect-307.custom.localhost';
$ruleId = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 307, 'site', $siteId);
$this->assertNotEmpty($ruleId);
$proxyClient = new Client();
$proxyClient->setEndpoint('http://appwrite.test');
$proxyClient->addHeader('x-appwrite-hostname', $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false);
$this->assertEquals(307, $response['headers']['status-code']);
$this->assertEquals('https://jsonplaceholder.typicode.com/todos/1', $response['headers']['location']);
$rules = $this->listRules([
'queries' => [
Query::equal('type', ['redirect'])->toString(),
Query::equal('trigger', ['manual'])->toString(),
Query::equal('deploymentResourceType', ['site'])->toString(),
Query::equal('deploymentResourceId', [$siteId])->toString(),
],
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertEquals(2, $rules['body']['total']);
$this->assertEquals($function['headers']['status-code'], 201, 'Setup function failed with status code: ' . $function['headers']['status-code'] . ' and response: ' . json_encode($function['body'], JSON_PRETTY_PRINT));
$this->cleanupSite($siteId);
$this->cleanupRule($ruleId);
}
$functionId = $function['body']['$id'];
public function testCreateFunctionRule(): void
{
$domain = \uniqid() . '-function.custom.localhost';
// Deployment
$deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]), [
'code' => $this->packageFunction('basic'),
'activate' => 'true'
]);
$proxyClient = new Client();
$proxyClient->setEndpoint('http://appwrite.test');
$proxyClient->addHeader('x-appwrite-hostname', $domain);
$this->assertEquals($deployment['headers']['status-code'], 202, 'Setup deployment failed with status code: ' . $deployment['headers']['status-code'] . ' and response: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT));
$deploymentId = $deployment['body']['$id'] ?? '';
$response = $proxyClient->call(Client::METHOD_GET, '/ping');
$this->assertEquals(401, $response['headers']['status-code']);
$setup = $this->setupFunction();
$functionId = $setup['functionId'];
$deploymentId = $setup['deploymentId'];
$this->assertNotEmpty($functionId);
$this->assertNotEmpty($deploymentId);
$ruleId = $this->setupFunctionRule($domain, $functionId);
$this->assertNotEmpty($ruleId);
$response = $proxyClient->call(Client::METHOD_GET, '/ping');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($functionId, $response['body']['APPWRITE_FUNCTION_ID']);
$this->cleanupRule($ruleId);
$this->cleanupFunction($functionId);
$this->assertEventually(function () use ($functionId, $deploymentId) {
$function = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]));
$this->assertEquals($deploymentId, $function['body']['deploymentId'], 'Deployment is not activated, deployment: ' . json_encode($function['body'], JSON_PRETTY_PRINT));
}, 100000, 500);
$rules = $this->listRules([
'queries' => [
Query::limit(1)->toString(),
Query::equal('type', ['deployment'])->toString(),
Query::equal('deploymentResourceType', ['function'])->toString(),
Query::equal('deploymentResourceId', [$functionId])->toString(),
]
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertEquals(0, $rules['body']['total']);
$this->assertCount(0, $rules['body']['rules']);
return ['functionId' => $functionId, 'deploymentId' => $deploymentId];
$rules = $this->listRules([
'queries' => [
Query::limit(1)->toString(),
Query::equal('type', ['deployment'])->toString(),
Query::equal('deploymentId', [$deploymentId])->toString()
]
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertEquals(0, $rules['body']['total']);
$this->assertCount(0, $rules['body']['rules']);
});
}
private function packageSite(string $site): CURLFile
public function testCreateSiteRule(): void
{
$stdout = '';
$stderr = '';
$domain = \uniqid() . '-site.custom.localhost';
$folderPath = realpath(__DIR__ . '/../../../resources/sites') . "/$site";
$tarPath = "$folderPath/code.tar.gz";
$proxyClient = new Client();
$proxyClient->setEndpoint('http://appwrite.test');
$proxyClient->addHeader('x-appwrite-hostname', $domain);
Console::execute("cd $folderPath && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $stdout, $stderr);
$response = $proxyClient->call(Client::METHOD_GET, '/contact');
$this->assertEquals(401, $response['headers']['status-code']);
if (filesize($tarPath) > 1024 * 1024 * 5) {
throw new \Exception('Code package is too large. Use the chunked upload method instead.');
$setup = $this->setupSite();
$siteId = $setup['siteId'];
$deploymentId = $setup['deploymentId'];
$this->assertNotEmpty($siteId);
$this->assertNotEmpty($deploymentId);
$ruleId = $this->setupSiteRule($domain, $siteId);
$this->assertNotEmpty($ruleId);
$rule = $this->getRule($ruleId);
$this->assertSame(200, $rule['headers']['status-code']);
$this->assertSame('unverified', $rule['body']['status']);
$response = $proxyClient->call(Client::METHOD_GET, '/contact');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertStringContainsString('Contact page', $response['body']);
// Wildcard domains automatically get verified status
$domains = [
\uniqid() . '.sites.localhost',
\uniqid() . '.rebranded.localhost',
];
foreach ($domains as $domain) {
$wildcardRuleId = $this->setupSiteRule($domain, $siteId);
$this->assertNotEmpty($wildcardRuleId);
$rule = $this->getRule($wildcardRuleId);
$this->assertSame(200, $rule['headers']['status-code']);
$this->assertSame('verified', $rule['body']['status']);
$this->cleanupRule($wildcardRuleId);
}
return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath));
$rules = $this->listRules([
'queries' => [
Query::limit(1)->toString(),
Query::equal('trigger', ['deployment'])->toString(),
Query::equal('type', ['deployment'])->toString(),
Query::equal('deploymentResourceType', ['site'])->toString(),
Query::equal('deploymentResourceId', [$siteId])->toString(),
]
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertGreaterThan(0, $rules['body']['total']);
$this->cleanupRule($ruleId);
$this->cleanupSite($siteId);
$this->assertEventually(function () use ($siteId, $deploymentId) {
$rules = $this->listRules([
'queries' => [
Query::limit(1)->toString(),
Query::equal('type', ['deployment'])->toString(),
Query::equal('deploymentResourceType', ['site'])->toString(),
Query::equal('deploymentResourceId', [$siteId])->toString(),
]
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertEquals(0, $rules['body']['total']);
$this->assertCount(0, $rules['body']['rules']);
$rules = $this->listRules([
'queries' => [
Query::limit(1)->toString(),
Query::equal('type', ['deployment'])->toString(),
Query::equal('deploymentId', [$deploymentId])->toString()
]
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertEquals(0, $rules['body']['total']);
$this->assertCount(0, $rules['body']['rules']);
});
}
private function packageFunction(string $function): CURLFile
public function testCreateSiteBranchRule(): void
{
$stdout = '';
$stderr = '';
$domain = \uniqid() . '-site-branch.custom.localhost';
$folderPath = realpath(__DIR__ . '/../../../resources/functions') . "/$function";
$tarPath = "$folderPath/code.tar.gz";
$setup = $this->setupSite();
$siteId = $setup['siteId'];
$deploymentId = $setup['deploymentId'];
Console::execute("cd $folderPath && tar --exclude code.tar.gz --exclude node_modules -czf code.tar.gz .", '', $stdout, $stderr);
$this->assertNotEmpty($siteId);
$this->assertNotEmpty($deploymentId);
if (filesize($tarPath) > 1024 * 1024 * 5) {
throw new \Exception('Code package is too large. Use the chunked upload method instead.');
$ruleId = $this->setupSiteRule($domain, $siteId, 'dev');
$this->assertNotEmpty($ruleId);
$rule = $this->getRule($ruleId);
$this->assertEquals(200, $rule['headers']['status-code']);
$this->cleanupRule($ruleId);
}
public function testCreateFunctionBranchRule(): void
{
$domain = \uniqid() . '-function-branch.custom.localhost';
$setup = $this->setupFunction();
$functionId = $setup['functionId'];
$deploymentId = $setup['deploymentId'];
$this->assertNotEmpty($functionId);
$this->assertNotEmpty($deploymentId);
$ruleId = $this->setupFunctionRule($domain, $functionId, 'dev');
$this->assertNotEmpty($ruleId);
$rule = $this->getRule($ruleId);
$this->assertEquals(200, $rule['headers']['status-code']);
$this->cleanupRule($ruleId);
$this->cleanupFunction($functionId);
}
public function testUpdateRule(): void
{
// Create function appwrite-network domain
$functionsDomain = \explode(',', System::getEnv('_APP_DOMAIN_FUNCTIONS', ''))[0];
$domain = \uniqid() . '-cname-api.' . $functionsDomain;
$rule = $this->createAPIRule($domain);
$this->assertEquals(201, $rule['headers']['status-code']);
$this->assertEquals('verified', $rule['body']['status']);
$this->cleanupRule($rule['body']['$id']);
// Create site appwrite-network domain
$sitesDomain = \explode(',', System::getEnv('_APP_DOMAIN_SITES', ''))[0];
$domain = \uniqid() . '-cname-api.' . $sitesDomain;
$rule = $this->createAPIRule($domain);
$this->assertEquals(201, $rule['headers']['status-code']);
$this->assertEquals('verified', $rule['body']['status']);
$this->cleanupRule($rule['body']['$id']);
// Create + update
$domain = \uniqid() . '-cname-api.custom.com';
$rule = $this->createAPIRule($domain);
$this->assertEquals(201, $rule['headers']['status-code']);
$this->assertEquals('unverified', $rule['body']['status']);
$ruleId = $rule['body']['$id'];
$rule = $this->updateRuleStatus($ruleId);
$this->assertEquals(400, $rule['headers']['status-code']);
$this->cleanupRule($ruleId);
}
public function testGetRule()
{
$domain = \uniqid() . '-get.custom.localhost';
$ruleId = $this->setupAPIRule($domain);
$this->assertNotEmpty($ruleId);
$rule = $this->getRule($ruleId);
$this->assertEquals(200, $rule['headers']['status-code']);
$this->assertEquals($domain, $rule['body']['domain']);
$this->assertEquals('manual', $rule['body']['trigger']);
$this->assertArrayHasKey('$id', $rule['body']);
$this->assertArrayHasKey('domain', $rule['body']);
$this->assertArrayHasKey('type', $rule['body']);
$this->assertArrayHasKey('redirectUrl', $rule['body']);
$this->assertArrayHasKey('redirectStatusCode', $rule['body']);
$this->assertArrayHasKey('deploymentResourceType', $rule['body']);
$this->assertArrayHasKey('deploymentId', $rule['body']);
$this->assertArrayHasKey('deploymentResourceId', $rule['body']);
$this->assertArrayHasKey('deploymentVcsProviderBranch', $rule['body']);
$this->assertArrayHasKey('logs', $rule['body']);
$this->assertArrayHasKey('renewAt', $rule['body']);
$this->cleanupRule($ruleId);
}
public function testListRules()
{
$rules = $this->listRules();
$this->assertEquals(200, $rules['headers']['status-code']);
foreach ($rules['body']['rules'] as $rule) {
$rule = $this->deleteRule($rule['$id']);
$this->assertEquals(204, $rule['headers']['status-code']);
}
return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath));
$rules = $this->listRules();
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertEquals(0, $rules['body']['total']);
$this->assertCount(0, $rules['body']['rules']);
$rule1Domain = \uniqid() . '-list1.custom.localhost';
$rule1Id = $this->setupAPIRule($rule1Domain);
$this->assertNotEmpty($rule1Id);
$rules = $this->listRules();
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertEquals(1, $rules['body']['total']);
$this->assertCount(1, $rules['body']['rules']);
$this->assertEquals($rule1Domain, $rules['body']['rules'][0]['domain']);
$this->assertEquals('manual', $rules['body']['rules'][0]['trigger']);
$this->assertArrayHasKey('$id', $rules['body']['rules'][0]);
$this->assertArrayHasKey('domain', $rules['body']['rules'][0]);
$this->assertArrayHasKey('type', $rules['body']['rules'][0]);
$this->assertArrayHasKey('redirectUrl', $rules['body']['rules'][0]);
$this->assertArrayHasKey('redirectStatusCode', $rules['body']['rules'][0]);
$this->assertArrayHasKey('deploymentResourceType', $rules['body']['rules'][0]);
$this->assertArrayHasKey('deploymentId', $rules['body']['rules'][0]);
$this->assertArrayHasKey('deploymentResourceId', $rules['body']['rules'][0]);
$this->assertArrayHasKey('deploymentVcsProviderBranch', $rules['body']['rules'][0]);
$this->assertArrayHasKey('logs', $rules['body']['rules'][0]);
$this->assertArrayHasKey('renewAt', $rules['body']['rules'][0]);
$rule2Domain = \uniqid() . '-list1.custom.localhost';
$rule2Id = $this->setupAPIRule($rule2Domain);
$this->assertNotEmpty($rule2Id);
$rules = $this->listRules();
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertEquals(2, $rules['body']['total']);
$this->assertCount(2, $rules['body']['rules']);
$rules = $this->listRules([
'queries' => [
Query::limit(1)->toString()
]
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertEquals(2, $rules['body']['total']);
$this->assertCount(1, $rules['body']['rules']);
$rules = $this->listRules([
'queries' => [
Query::equal('$id', [$rule1Id])->toString()
]
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertCount(1, $rules['body']['rules']);
$this->assertEquals($rule1Domain, $rules['body']['rules'][0]['domain']);
$rules = $this->listRules([
'queries' => [
Query::orderDesc('$id')->toString()
]
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertCount(2, $rules['body']['rules']);
$this->assertEquals($rule2Id, $rules['body']['rules'][0]['$id']);
$rules = $this->listRules([
'queries' => [
Query::equal('domain', [$rule2Domain])->toString()
]
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertCount(1, $rules['body']['rules']);
$this->assertEquals($rule2Id, $rules['body']['rules'][0]['$id']);
$rules = $this->listRules([
'search' => $rule1Domain,
'queries' => [ Query::orderDesc('$createdAt')->toString() ]
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$ruleIds = \array_column($rules['body']['rules'], '$id');
$this->assertContains($rule1Id, $ruleIds);
$rules = $this->listRules([
'search' => $rule2Domain,
'queries' => [ Query::orderDesc('$createdAt')->toString() ]
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$ruleIds = \array_column($rules['body']['rules'], '$id');
$this->assertContains($rule2Id, $ruleIds);
$rules = $this->listRules([
'search' => $rule1Id,
'queries' => [ Query::orderDesc('$createdAt')->toString() ]
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$ruleDomains = \array_column($rules['body']['rules'], 'domain');
$this->assertContains($rule1Domain, $ruleDomains);
$rules = $this->listRules([
'search' => $rule2Id,
'queries' => [ Query::orderDesc('$createdAt')->toString() ]
]);
$this->assertEquals(200, $rules['headers']['status-code']);
$ruleDomains = \array_column($rules['body']['rules'], 'domain');
$this->assertContains($rule2Domain, $ruleDomains);
$rules = $this->listRules();
$this->assertEquals(200, $rules['headers']['status-code']);
foreach ($rules['body']['rules'] as $rule) {
$rule = $this->deleteRule($rule['$id']);
$this->assertEquals(204, $rule['headers']['status-code']);
}
$rules = $this->listRules();
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertEquals(0, $rules['body']['total']);
$this->assertCount(0, $rules['body']['rules']);
}
public function testRuleVerification(): void
{
// 1. Site rule can verify
$site = $this->setupSite();
$siteId = $site['siteId'];
$rule = $this->createSiteRule('stage-site.webapp.com', $siteId);
$this->assertEquals(201, $rule['headers']['status-code']);
$this->assertEquals('verifying', $rule['body']['status']);
$this->assertEmpty($rule['body']['logs']);
$this->assertNotEmpty($rule['body']['$id']);
$ruleId = $rule['body']['$id'];
$rule = $this->updateRuleStatus($ruleId);
$this->assertEquals(200, $rule['headers']['status-code']);
$this->assertEquals($ruleId, $rule['body']['$id']);
$this->assertEquals('verifying', $rule['body']['status']);
$this->assertEmpty($rule['body']['logs']);
$this->cleanupRule($rule['body']['$id']);
$this->cleanupSite($siteId);
// 2. Function rule can verify
$function = $this->setupFunction();
$functionId = $function['functionId'];
$rule = $this->createFunctionRule('stage-function.webapp.com', $functionId);
$this->assertEquals(201, $rule['headers']['status-code']);
$this->assertEquals('verifying', $rule['body']['status']);
$this->assertEmpty($rule['body']['logs']);
$this->cleanupRule($rule['body']['$id']);
$rule = $this->createAPIRule('stage-site.webapp.com');
$this->assertEquals(201, $rule['headers']['status-code']);
$this->assertEquals('unverified', $rule['body']['status']);
$this->assertStringContainsString('has incorrect CNAME value', $rule['body']['logs']);
$this->cleanupRule($rule['body']['$id']);
$this->cleanupFunction($functionId);
// 3. Wrong A record fails to verify
$rule = $this->createAPIRule('wrong-a-webapp.com');
$this->assertEquals(201, $rule['headers']['status-code']);
$this->assertEquals('unverified', $rule['body']['status']);
$this->assertStringContainsString('is missing CNAME record', $rule['body']['logs']);
$ruleId = $rule['body']['$id'];
$rule = $this->updateRuleStatus($ruleId);
$this->assertEquals(400, $rule['headers']['status-code']);
$this->assertStringContainsString('is missing CNAME record', $rule['body']['message']);
$rule = $this->getRule($ruleId);
$this->assertEquals(200, $rule['headers']['status-code']);
$this->assertEquals('unverified', $rule['body']['status']);
$this->cleanupRule($ruleId);
// 4. Correct A record can verify
$rule = $this->createAPIRule('webapp.com');
$this->assertEquals(201, $rule['headers']['status-code']);
$this->assertEquals('verifying', $rule['body']['status']);
$this->assertEmpty($rule['body']['logs']);
$this->cleanupRule($rule['body']['$id']);
// 5. Correct CNAME record can verify (no CAA record)
$rule = $this->createAPIRule('stage.webapp.com');
$this->assertEquals(201, $rule['headers']['status-code']);
$this->assertEquals('verifying', $rule['body']['status']);
$this->assertEmpty($rule['body']['logs']);
$this->cleanupRule($rule['body']['$id']);
// 6. Missing CNAME record fails to verify
$rule = $this->createAPIRule('stage-missing-cname.webapp.com');
$this->assertEquals(201, $rule['headers']['status-code']);
$this->assertEquals('unverified', $rule['body']['status']);
$this->assertStringContainsString('is missing CNAME record', $rule['body']['logs']);
$ruleId = $rule['body']['$id'];
$rule = $this->updateRuleStatus($ruleId);
$this->assertEquals(400, $rule['headers']['status-code']);
$this->assertStringContainsString('is missing CNAME record', $rule['body']['message']);
$rule = $this->getRule($ruleId);
$this->assertEquals(200, $rule['headers']['status-code']);
$this->assertEquals('unverified', $rule['body']['status']);
$this->cleanupRule($ruleId);
// 7. Wrong CNAME record fails to verify
$rule = $this->createAPIRule('stage-wrong-cname.webapp.com');
$this->assertEquals(201, $rule['headers']['status-code']);
$this->assertEquals('unverified', $rule['body']['status']);
$this->assertStringContainsString('has incorrect CNAME value', $rule['body']['logs']);
$ruleId = $rule['body']['$id'];
$rule = $this->updateRuleStatus($ruleId);
$this->assertEquals(400, $rule['headers']['status-code']);
$this->assertStringContainsString('has incorrect CNAME value', $rule['body']['message']);
$rule = $this->getRule($ruleId);
$this->assertEquals(200, $rule['headers']['status-code']);
$this->assertEquals('unverified', $rule['body']['status']);
$this->cleanupRule($ruleId);
// 8. Wrong CAA record fails to verify
$rule = $this->createAPIRule('stage-wrong-caa.webapp.com');
$this->assertEquals(201, $rule['headers']['status-code']);
$this->assertEquals('unverified', $rule['body']['status']);
$this->assertStringContainsString('has incorrect CAA value', $rule['body']['logs']);
$ruleId = $rule['body']['$id'];
$rule = $this->updateRuleStatus($ruleId);
$this->assertEquals(400, $rule['headers']['status-code']);
$this->assertStringContainsString('has incorrect CAA value', $rule['body']['message']);
$rule = $this->getRule($ruleId);
$this->assertEquals(200, $rule['headers']['status-code']);
$this->assertEquals('unverified', $rule['body']['status']);
$this->cleanupRule($ruleId);
// 9. Correct CAA record can verify
$rule = $this->createAPIRule('stage-correct-caa.webapp.com');
$this->assertEquals(201, $rule['headers']['status-code']);
$this->assertEquals('verifying', $rule['body']['status']);
$this->assertEmpty($rule['body']['logs']);
$this->cleanupRule($rule['body']['$id']);
}
public function testUpdateRuleVerificationWithSameDataUpdatesTimestamp(): void
{
$domain = \uniqid() . '-timestamp-test.webapp.com';
$rule = $this->createAPIRule($domain);
$this->assertEquals(201, $rule['headers']['status-code']);
$this->assertEquals('unverified', $rule['body']['status']);
$this->assertNotEmpty($rule['body']['logs']);
$ruleId = $rule['body']['$id'];
$initialUpdatedAt = $rule['body']['$updatedAt'];
$initiallogs = $rule['body']['logs'];
sleep(1);
$updatedRule = $this->updateRuleStatus($ruleId);
$this->assertEquals(400, $updatedRule['headers']['status-code']);
$this->assertStringContainsString($initiallogs, $updatedRule['body']['message']);
$ruleAfterUpdate = $this->getRule($ruleId);
$this->assertEquals(200, $ruleAfterUpdate['headers']['status-code']);
$this->assertEquals('unverified', $ruleAfterUpdate['body']['status']);
$this->assertEquals($initiallogs, $ruleAfterUpdate['body']['logs']);
$this->assertNotEquals($initialUpdatedAt, $ruleAfterUpdate['body']['$updatedAt']);
$initialTime = new \DateTime($initialUpdatedAt);
$updatedTime = new \DateTime($ruleAfterUpdate['body']['$updatedAt']);
$this->assertGreaterThan($initialTime, $updatedTime);
$this->cleanupRule($ruleId);
}
}

Some files were not shown because too many files have changed in this diff Show More