strapi-plugin-xquik 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Xquik
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,151 @@
1
- # Temporary Holding Version
1
+ # X API & Twitter API Plugin for Strapi 5
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ <img src="https://raw.githubusercontent.com/Xquik-dev/strapi-plugin-xquik/main/assets/logo.png" alt="Xquik logo" width="96" height="96">
4
+
5
+ [Xquik](https://xquik.com) is the world's fastest and cheapest X Twitter data API with the most complete X data.
6
+ Searches & extractions charge only for delivered, unique, filter-matching results.
7
+
8
+ Xquik is an independent third-party service. Not affiliated with X Corp. "Twitter" and "X" are trademarks of X Corp.
9
+ Xquik makes this plugin. Strapi doesn't make or endorse it.
10
+
11
+ This plugin adds an `xquik` service to your Strapi 5 server.
12
+ The service searches X posts & reads X profiles.
13
+ It runs on the server only. It adds no admin pages, routes or content types.
14
+
15
+ ## Requirements
16
+
17
+ - Strapi 5
18
+ - Node.js 20 to 26
19
+ - An Xquik API key from your dashboard at [xquik.com](https://xquik.com)
20
+
21
+ ## Install
22
+
23
+ ```sh
24
+ npm install strapi-plugin-xquik
25
+ ```
26
+
27
+ Strapi 5 enables the plugin on its next start.
28
+
29
+ ## Configure
30
+
31
+ Add your API key to your app's `.env` file.
32
+
33
+ ```sh
34
+ XQUIK_API_KEY=xq_...
35
+ ```
36
+
37
+ The plugin reads `XQUIK_API_KEY` by default.
38
+ To set the key another way, set `apiKey` in `config/plugins.js`.
39
+
40
+ ```js
41
+ module.exports = ({ env }) => ({
42
+ xquik: {
43
+ config: {
44
+ apiKey: env("XQUIK_API_KEY"),
45
+ },
46
+ },
47
+ });
48
+ ```
49
+
50
+ | Option | Type | Default | Use |
51
+ | -------- | ------ | ------------------------ | ---------------------------------------------- |
52
+ | `apiKey` | string | `XQUIK_API_KEY`, or `""` | The key the plugin sends in `x-api-key`. |
53
+
54
+ The plugin reads the key on each call.
55
+ With no key, a call fails before it sends a request.
56
+
57
+ ## Search posts
58
+
59
+ Call the service from any controller, service or lifecycle hook.
60
+
61
+ ```js
62
+ const page = await strapi
63
+ .plugin("xquik")
64
+ .service("xquik")
65
+ .searchPosts({ q: "from:NASA", limit: 10 });
66
+ ```
67
+
68
+ `searchPosts(query)` takes these fields.
69
+
70
+ | Field | Type | Use |
71
+ | ----------- | ------ | ------------------------------------------------------------------ |
72
+ | `q` | string | Required. A search query, a post ID or a post URL. |
73
+ | `limit` | number | Most posts to return, from 1 to 10,000. The API default is 20. |
74
+ | `cursor` | string | The `next_cursor` of the previous page. |
75
+ | `queryType` | string | `Latest` lists the newest posts first. `Top` lists high-engagement posts first.|
76
+
77
+ It also passes every other search filter in the [search API reference](https://docs.xquik.com/api-reference/x/search-tweets).
78
+ Examples are `language`, `minLikes`, `sinceDate` & `untilDate`.
79
+ `q` takes [X search operators](https://github.com/Xquik-dev/x-twitter-advanced-search), such as `from:` & `since:`.
80
+
81
+ It returns `{ tweets, has_next_page, next_cursor }`.
82
+ Each post has an `id`. It also has fields such as `text`, `createdAt`, `likeCount`, `viewCount` & `author`.
83
+
84
+ To read the next page, pass `next_cursor` as `cursor` with the same filters.
85
+ Stop when `has_next_page` is `false`.
86
+
87
+ ## Read a profile
88
+
89
+ ```js
90
+ const profile = await strapi.plugin("xquik").service("xquik").getUser("NASA");
91
+ ```
92
+
93
+ `getUser(id)` takes a user ID, a username with or without `@`, or a profile URL such as `x.com/nasa`.
94
+ It returns the profile with `id`, `username` & `name`.
95
+ It also has fields such as `description`, `followers`, `following` & `verified`.
96
+ The [profile API reference](https://docs.xquik.com/api-reference/x/get-user) lists every field.
97
+
98
+ ## Handle errors
99
+
100
+ Each failure throws an `XquikError`.
101
+ Its `code` names the failure. Its `status` holds the HTTP status when the API answered.
102
+
103
+ | `code` | `status` | Next step |
104
+ | ---------------------- | ----------- | ----------------------------------------------------------- |
105
+ | `missing_api_key` | `undefined` | Set `XQUIK_API_KEY` or `apiKey`. |
106
+ | `network_error` | `undefined` | The request failed or took over 30 seconds. Retry. |
107
+ | `unauthenticated` | `401` | Check your API key. |
108
+ | `insufficient_credits` | `402` | Add credits in your Xquik dashboard. |
109
+ | `rate_limit_exceeded` | `429` | Wait, then retry. |
110
+ | `http_error` | The status | The API sent an error without a JSON body. Retry. |
111
+ | `invalid_response` | The status | The response didn't match the contract. Update the plugin. |
112
+
113
+ Other API errors keep the API's own `code` & `message`.
114
+ The [API reference](https://docs.xquik.com/api-reference/overview) lists every code.
115
+
116
+ This controller returns search results & turns an `XquikError` into a JSON error.
117
+
118
+ ```js
119
+ module.exports = ({ strapi }) => ({
120
+ async search(ctx) {
121
+ try {
122
+ ctx.body = await strapi
123
+ .plugin("xquik")
124
+ .service("xquik")
125
+ .searchPosts({ q: ctx.query.q, limit: 10 });
126
+ } catch (error) {
127
+ if (error.name !== "XquikError") {
128
+ throw error;
129
+ }
130
+ ctx.status = error.status ?? 500;
131
+ ctx.body = { error: { code: error.code, message: error.message } };
132
+ }
133
+ },
134
+ });
135
+ ```
136
+
137
+ ## Develop
138
+
139
+ ```sh
140
+ bin/check-local
141
+ ```
142
+
143
+ `bin/check-local` runs every check in order.
144
+ It installs, then checks format, lint, types & tests.
145
+ It builds & verifies the plugin, then runs `npm audit` & `npm pack --dry-run`.
146
+ Last, it starts a fresh Strapi 5 app with the packed plugin & calls the service over HTTP.
147
+ With `XQUIK_API_KEY` set, that last check calls the live API.
148
+
149
+ ## License
150
+
151
+ [MIT](LICENSE)
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
3
+ const zod = require("zod");
4
+ const config = {
5
+ default: ({ env }) => ({
6
+ apiKey: env("XQUIK_API_KEY", "")
7
+ }),
8
+ validator(config2) {
9
+ if (typeof config2.apiKey !== "string") {
10
+ throw new TypeError("Set the xquik apiKey config to a string.");
11
+ }
12
+ }
13
+ };
14
+ const XQUIK_URL = "https://xquik.com";
15
+ const API_URL = `${XQUIK_URL}/api/v1/x`;
16
+ const REQUEST_TIMEOUT_MS = 3e4;
17
+ const NO_USER_IDS = /* @__PURE__ */ new Set(["", ".", ".."]);
18
+ class XquikError extends Error {
19
+ name = "XquikError";
20
+ code;
21
+ status;
22
+ constructor(code, message, status, options) {
23
+ super(message, options);
24
+ this.code = code;
25
+ this.status = status;
26
+ }
27
+ }
28
+ const postPageSchema = zod.z.looseObject({
29
+ tweets: zod.z.array(zod.z.looseObject({ id: zod.z.string() })),
30
+ has_next_page: zod.z.boolean(),
31
+ next_cursor: zod.z.string()
32
+ });
33
+ const userProfileSchema = zod.z.looseObject({
34
+ id: zod.z.string(),
35
+ username: zod.z.string(),
36
+ name: zod.z.string()
37
+ });
38
+ const failureSchema = zod.z.looseObject({
39
+ error: zod.z.string(),
40
+ message: zod.z.string().optional()
41
+ });
42
+ const failureFrom = (status, body) => {
43
+ const failure = failureSchema.safeParse(body);
44
+ return failure.success ? new XquikError(
45
+ failure.data.error,
46
+ failure.data.message ?? "Xquik rejected the request. Check the error code.",
47
+ status
48
+ ) : new XquikError(
49
+ "http_error",
50
+ "Xquik returned an unknown error. Retry later.",
51
+ status
52
+ );
53
+ };
54
+ const readApi = async (strapi, path, schema) => {
55
+ const apiKey = strapi.plugin("xquik").config("apiKey");
56
+ if (apiKey === "") {
57
+ throw new XquikError(
58
+ "missing_api_key",
59
+ `Set XQUIK_API_KEY. Get a key at ${XQUIK_URL}.`
60
+ );
61
+ }
62
+ const response = await fetch(`${API_URL}${path}`, {
63
+ headers: { accept: "application/json", "x-api-key": apiKey },
64
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
65
+ redirect: "error"
66
+ }).catch((cause) => {
67
+ throw new XquikError(
68
+ "network_error",
69
+ "Xquik did not answer. Check the network and retry.",
70
+ void 0,
71
+ { cause }
72
+ );
73
+ });
74
+ const body = await response.json().catch(() => null);
75
+ if (!response.ok) {
76
+ throw failureFrom(response.status, body);
77
+ }
78
+ const parsed = schema.safeParse(body);
79
+ if (!parsed.success) {
80
+ throw new XquikError(
81
+ "invalid_response",
82
+ "Xquik sent an unexpected response. Update the plugin or retry.",
83
+ response.status
84
+ );
85
+ }
86
+ return parsed.data;
87
+ };
88
+ const xquikService = ({ strapi }) => ({
89
+ searchPosts(query) {
90
+ const params = new URLSearchParams();
91
+ for (const [name, value] of Object.entries(query)) {
92
+ if (value !== void 0) {
93
+ params.set(name, String(value));
94
+ }
95
+ }
96
+ return readApi(
97
+ strapi,
98
+ `/tweets/search?${params.toString()}`,
99
+ postPageSchema
100
+ );
101
+ },
102
+ getUser(id) {
103
+ if (NO_USER_IDS.has(id)) {
104
+ return Promise.reject(
105
+ new XquikError("invalid_user", "Pass a user ID or username.")
106
+ );
107
+ }
108
+ return readApi(
109
+ strapi,
110
+ `/users/${encodeURIComponent(id)}`,
111
+ userProfileSchema
112
+ );
113
+ }
114
+ });
115
+ const index = {
116
+ config,
117
+ services: { xquik: xquikService }
118
+ };
119
+ exports.XquikError = XquikError;
120
+ exports.default = index;
@@ -0,0 +1,120 @@
1
+ import { z } from "zod";
2
+ const config = {
3
+ default: ({ env }) => ({
4
+ apiKey: env("XQUIK_API_KEY", "")
5
+ }),
6
+ validator(config2) {
7
+ if (typeof config2.apiKey !== "string") {
8
+ throw new TypeError("Set the xquik apiKey config to a string.");
9
+ }
10
+ }
11
+ };
12
+ const XQUIK_URL = "https://xquik.com";
13
+ const API_URL = `${XQUIK_URL}/api/v1/x`;
14
+ const REQUEST_TIMEOUT_MS = 3e4;
15
+ const NO_USER_IDS = /* @__PURE__ */ new Set(["", ".", ".."]);
16
+ class XquikError extends Error {
17
+ name = "XquikError";
18
+ code;
19
+ status;
20
+ constructor(code, message, status, options) {
21
+ super(message, options);
22
+ this.code = code;
23
+ this.status = status;
24
+ }
25
+ }
26
+ const postPageSchema = z.looseObject({
27
+ tweets: z.array(z.looseObject({ id: z.string() })),
28
+ has_next_page: z.boolean(),
29
+ next_cursor: z.string()
30
+ });
31
+ const userProfileSchema = z.looseObject({
32
+ id: z.string(),
33
+ username: z.string(),
34
+ name: z.string()
35
+ });
36
+ const failureSchema = z.looseObject({
37
+ error: z.string(),
38
+ message: z.string().optional()
39
+ });
40
+ const failureFrom = (status, body) => {
41
+ const failure = failureSchema.safeParse(body);
42
+ return failure.success ? new XquikError(
43
+ failure.data.error,
44
+ failure.data.message ?? "Xquik rejected the request. Check the error code.",
45
+ status
46
+ ) : new XquikError(
47
+ "http_error",
48
+ "Xquik returned an unknown error. Retry later.",
49
+ status
50
+ );
51
+ };
52
+ const readApi = async (strapi, path, schema) => {
53
+ const apiKey = strapi.plugin("xquik").config("apiKey");
54
+ if (apiKey === "") {
55
+ throw new XquikError(
56
+ "missing_api_key",
57
+ `Set XQUIK_API_KEY. Get a key at ${XQUIK_URL}.`
58
+ );
59
+ }
60
+ const response = await fetch(`${API_URL}${path}`, {
61
+ headers: { accept: "application/json", "x-api-key": apiKey },
62
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
63
+ redirect: "error"
64
+ }).catch((cause) => {
65
+ throw new XquikError(
66
+ "network_error",
67
+ "Xquik did not answer. Check the network and retry.",
68
+ void 0,
69
+ { cause }
70
+ );
71
+ });
72
+ const body = await response.json().catch(() => null);
73
+ if (!response.ok) {
74
+ throw failureFrom(response.status, body);
75
+ }
76
+ const parsed = schema.safeParse(body);
77
+ if (!parsed.success) {
78
+ throw new XquikError(
79
+ "invalid_response",
80
+ "Xquik sent an unexpected response. Update the plugin or retry.",
81
+ response.status
82
+ );
83
+ }
84
+ return parsed.data;
85
+ };
86
+ const xquikService = ({ strapi }) => ({
87
+ searchPosts(query) {
88
+ const params = new URLSearchParams();
89
+ for (const [name, value] of Object.entries(query)) {
90
+ if (value !== void 0) {
91
+ params.set(name, String(value));
92
+ }
93
+ }
94
+ return readApi(
95
+ strapi,
96
+ `/tweets/search?${params.toString()}`,
97
+ postPageSchema
98
+ );
99
+ },
100
+ getUser(id) {
101
+ if (NO_USER_IDS.has(id)) {
102
+ return Promise.reject(
103
+ new XquikError("invalid_user", "Pass a user ID or username.")
104
+ );
105
+ }
106
+ return readApi(
107
+ strapi,
108
+ `/users/${encodeURIComponent(id)}`,
109
+ userProfileSchema
110
+ );
111
+ }
112
+ });
113
+ const index = {
114
+ config,
115
+ services: { xquik: xquikService }
116
+ };
117
+ export {
118
+ XquikError,
119
+ index as default
120
+ };
@@ -0,0 +1,11 @@
1
+ declare const _default: {
2
+ default: ({ env }: {
3
+ env: (key: string, fallback: string) => string;
4
+ }) => {
5
+ apiKey: string;
6
+ };
7
+ validator(config: {
8
+ apiKey: unknown;
9
+ }): void;
10
+ };
11
+ export default _default;
@@ -0,0 +1,23 @@
1
+ export { XquikError } from './services/xquik';
2
+ export type { Post, PostPage, SearchPostsQuery, UserProfile, } from './services/xquik';
3
+ declare const _default: {
4
+ config: {
5
+ default: ({ env }: {
6
+ env: (key: string, fallback: string) => string;
7
+ }) => {
8
+ apiKey: string;
9
+ };
10
+ validator(config: {
11
+ apiKey: unknown;
12
+ }): void;
13
+ };
14
+ services: {
15
+ xquik: ({ strapi }: {
16
+ strapi: import('./services/xquik').StrapiHandle;
17
+ }) => {
18
+ searchPosts(query: import('.').SearchPostsQuery): Promise<import('.').PostPage>;
19
+ getUser(id: string): Promise<import('.').UserProfile>;
20
+ };
21
+ };
22
+ };
23
+ export default _default;
@@ -0,0 +1,69 @@
1
+ export declare class XquikError extends Error {
2
+ readonly name = "XquikError";
3
+ readonly code: string;
4
+ readonly status: number | undefined;
5
+ constructor(code: string, message: string, status?: number, options?: ErrorOptions);
6
+ }
7
+ export interface UserProfile {
8
+ id: string;
9
+ username: string;
10
+ name: string;
11
+ description?: string;
12
+ followers?: number;
13
+ following?: number;
14
+ verified?: boolean;
15
+ isBlueVerified?: boolean;
16
+ isVerified?: boolean;
17
+ profilePicture?: string;
18
+ profilePictureOriginal?: string;
19
+ coverPicture?: string;
20
+ location?: string;
21
+ url?: string;
22
+ createdAt?: string;
23
+ statusesCount?: number;
24
+ [field: string]: unknown;
25
+ }
26
+ export interface Post {
27
+ id: string;
28
+ text?: string;
29
+ createdAt?: string;
30
+ url?: string;
31
+ lang?: string;
32
+ likeCount?: number;
33
+ retweetCount?: number;
34
+ replyCount?: number;
35
+ quoteCount?: number;
36
+ viewCount?: number;
37
+ bookmarkCount?: number;
38
+ conversationId?: string;
39
+ isReply?: boolean;
40
+ isRetweet?: boolean;
41
+ isQuoteStatus?: boolean;
42
+ author?: UserProfile;
43
+ [field: string]: unknown;
44
+ }
45
+ export interface PostPage {
46
+ tweets: Post[];
47
+ has_next_page: boolean;
48
+ next_cursor: string;
49
+ [field: string]: unknown;
50
+ }
51
+ export interface SearchPostsQuery {
52
+ q: string;
53
+ limit?: number;
54
+ cursor?: string;
55
+ queryType?: string;
56
+ [filter: string]: string | number | boolean | undefined;
57
+ }
58
+ export interface StrapiHandle {
59
+ plugin(name: "xquik"): {
60
+ config(key: "apiKey"): string;
61
+ };
62
+ }
63
+ declare const xquikService: ({ strapi }: {
64
+ strapi: StrapiHandle;
65
+ }) => {
66
+ searchPosts(query: SearchPostsQuery): Promise<PostPage>;
67
+ getUser(id: string): Promise<UserProfile>;
68
+ };
69
+ export default xquikService;
package/package.json CHANGED
@@ -1,6 +1,70 @@
1
1
  {
2
2
  "name": "strapi-plugin-xquik",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.1",
4
+ "description": "Search X posts & read profiles from Strapi 5 with the Xquik X Twitter data API.",
5
+ "keywords": [
6
+ "strapi",
7
+ "strapi-plugin",
8
+ "strapi-5",
9
+ "xquik",
10
+ "x-twitter-data-api",
11
+ "twitter-api",
12
+ "x-api"
13
+ ],
14
+ "homepage": "https://xquik.com",
15
+ "bugs": {
16
+ "url": "https://github.com/Xquik-dev/strapi-plugin-xquik/issues"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/Xquik-dev/strapi-plugin-xquik.git"
21
+ },
22
+ "license": "MIT",
23
+ "author": "Xquik",
24
+ "type": "commonjs",
25
+ "exports": {
26
+ "./package.json": "./package.json",
27
+ "./strapi-server": {
28
+ "types": "./dist/server/src/index.d.ts",
29
+ "source": "./server/src/index.ts",
30
+ "import": "./dist/server/index.mjs",
31
+ "require": "./dist/server/index.js",
32
+ "default": "./dist/server/index.js"
33
+ }
34
+ },
35
+ "files": [
36
+ "dist"
37
+ ],
38
+ "scripts": {
39
+ "prepack": "strapi-plugin build",
40
+ "test": "bun test"
41
+ },
42
+ "dependencies": {
43
+ "zod": "^4.6.7"
44
+ },
45
+ "devDependencies": {
46
+ "@strapi/sdk-plugin": "6.1.1",
47
+ "@types/bun": "1.4.4",
48
+ "fast-check": "4.10.2",
49
+ "oxlint": "1.90.0",
50
+ "typescript": "5.9.3"
51
+ },
52
+ "peerDependencies": {
53
+ "@strapi/strapi": "^5"
54
+ },
55
+ "overrides": {
56
+ "minimatch": "11.0.4"
57
+ },
58
+ "engines": {
59
+ "node": ">=20.0.0 <=26.x.x"
60
+ },
61
+ "publishConfig": {
62
+ "access": "public"
63
+ },
64
+ "strapi": {
65
+ "kind": "plugin",
66
+ "name": "xquik",
67
+ "displayName": "Xquik",
68
+ "description": "Search X posts & read profiles from Strapi 5 with the Xquik X Twitter data API."
69
+ }
70
+ }