@deallony/shared 1.2.51 → 1.2.52

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.
@@ -0,0 +1,8 @@
1
+ export declare const MAX_AD_IMAGES = 30;
2
+ export declare const MAX_AD_VIDEOS = 1;
3
+ export declare const MAX_AD_VIDEO_BYTES: number;
4
+ export declare enum AdMediaIssueCode {
5
+ TOO_MANY_IMAGES = "too_many_images",
6
+ TOO_MANY_VIDEOS = "too_many_videos",
7
+ VIDEO_TOO_LARGE = "video_too_large"
8
+ }
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AdMediaIssueCode = exports.MAX_AD_VIDEO_BYTES = exports.MAX_AD_VIDEOS = exports.MAX_AD_IMAGES = void 0;
4
+ // Media is optional on every ad. These are the only limits, shared by every
5
+ // client and the API so they can never drift apart.
6
+ exports.MAX_AD_IMAGES = 30;
7
+ exports.MAX_AD_VIDEOS = 1;
8
+ exports.MAX_AD_VIDEO_BYTES = 50 * 1024 * 1024;
9
+ // Stable codes carried in `params.code` of adMediaSchema issues, so clients can
10
+ // show localized messages without re-implementing the rules.
11
+ var AdMediaIssueCode;
12
+ (function (AdMediaIssueCode) {
13
+ AdMediaIssueCode["TOO_MANY_IMAGES"] = "too_many_images";
14
+ AdMediaIssueCode["TOO_MANY_VIDEOS"] = "too_many_videos";
15
+ AdMediaIssueCode["VIDEO_TOO_LARGE"] = "video_too_large";
16
+ })(AdMediaIssueCode || (exports.AdMediaIssueCode = AdMediaIssueCode = {}));
@@ -1 +1,2 @@
1
1
  export * from "./Ad";
2
+ export * from "./AdMedia";
@@ -15,3 +15,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./Ad"), exports);
18
+ __exportStar(require("./AdMedia"), exports);
@@ -5,6 +5,7 @@ export declare const subjectTypeSchema: z.ZodEnum<{
5
5
  motors: "motors";
6
6
  used_products: "used_products";
7
7
  }>;
8
+ export declare const adMediaSchema: z.ZodNullable<z.ZodOptional<z.ZodArray<z.ZodAny>>>;
8
9
  export declare const createAdSchema: z.ZodObject<{
9
10
  motor: z.ZodNullable<z.ZodOptional<z.ZodObject<{
10
11
  ad_id: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.saveAdSchema = exports.updateAdSchema = exports.createAdSchema = exports.subjectTypeSchema = void 0;
3
+ exports.saveAdSchema = exports.updateAdSchema = exports.createAdSchema = exports.adMediaSchema = exports.subjectTypeSchema = void 0;
4
4
  const zod_1 = require("zod");
5
+ const AdMedia_1 = require("../../constants/ad/AdMedia");
6
+ const adMedia_1 = require("../../utils/adMedia");
5
7
  exports.subjectTypeSchema = zod_1.z.enum([
6
8
  "properties",
7
9
  "services",
@@ -130,6 +132,40 @@ const createServiceSchema = baseServiceSchema;
130
132
  const updateServiceSchema = baseServiceSchema.extend({
131
133
  id: zod_1.z.coerce.number().int().positive().optional(),
132
134
  });
135
+ // Media is optional: an ad may have no media, only one video, or only images.
136
+ exports.adMediaSchema = zod_1.z
137
+ .array(zod_1.z.any())
138
+ .superRefine((media, ctx) => {
139
+ const { images, videos } = (0, adMedia_1.countAdMedia)(media);
140
+ if (images > AdMedia_1.MAX_AD_IMAGES) {
141
+ ctx.addIssue({
142
+ code: "custom",
143
+ message: `An ad can contain up to ${AdMedia_1.MAX_AD_IMAGES} images`,
144
+ params: { code: AdMedia_1.AdMediaIssueCode.TOO_MANY_IMAGES, max: AdMedia_1.MAX_AD_IMAGES },
145
+ });
146
+ }
147
+ if (videos > AdMedia_1.MAX_AD_VIDEOS) {
148
+ ctx.addIssue({
149
+ code: "custom",
150
+ message: `An ad can contain up to ${AdMedia_1.MAX_AD_VIDEOS} video`,
151
+ params: { code: AdMedia_1.AdMediaIssueCode.TOO_MANY_VIDEOS, max: AdMedia_1.MAX_AD_VIDEOS },
152
+ });
153
+ }
154
+ media.forEach((item, index) => {
155
+ if ((0, adMedia_1.isAdVideoMedia)(item) &&
156
+ typeof (item === null || item === void 0 ? void 0 : item.size) === "number" &&
157
+ item.size > AdMedia_1.MAX_AD_VIDEO_BYTES) {
158
+ ctx.addIssue({
159
+ code: "custom",
160
+ path: [index],
161
+ message: `An ad video cannot exceed ${AdMedia_1.MAX_AD_VIDEO_BYTES / (1024 * 1024)} MB`,
162
+ params: { code: AdMedia_1.AdMediaIssueCode.VIDEO_TOO_LARGE, max: AdMedia_1.MAX_AD_VIDEO_BYTES },
163
+ });
164
+ }
165
+ });
166
+ })
167
+ .optional()
168
+ .nullable();
133
169
  const baseAdCommon = zod_1.z.object({
134
170
  title: zod_1.z.string().trim().optional(),
135
171
  status_id: zod_1.z.coerce.number().int().positive().optional(),
@@ -146,7 +182,7 @@ const baseAdCommon = zod_1.z.object({
146
182
  return undefined;
147
183
  return value;
148
184
  }, zod_1.z.coerce.number().optional().nullable()),
149
- media: zod_1.z.array(zod_1.z.any()).optional().nullable(),
185
+ media: exports.adMediaSchema,
150
186
  subject_type: exports.subjectTypeSchema.optional(),
151
187
  base_price: zod_1.z.coerce.number().optional().nullable(),
152
188
  contact_methods: zod_1.z
@@ -0,0 +1,5 @@
1
+ export declare const isAdVideoMedia: (media: unknown) => boolean;
2
+ export declare const countAdMedia: (media: unknown) => {
3
+ images: number;
4
+ videos: number;
5
+ };
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.countAdMedia = exports.isAdVideoMedia = void 0;
4
+ const VIDEO_EXTENSION = /\.(mp4|mov|webm|m4v|ogv)(?:[?#]|$)/i;
5
+ // Older records can omit mimetype: fall back to the URL extension, and count
6
+ // anything unknown as an image rather than accepting unlimited untyped media.
7
+ const isAdVideoMedia = (media) => {
8
+ if (!media || typeof media !== "object")
9
+ return false;
10
+ const item = media;
11
+ const mimetype = typeof item.mimetype === "string" ? item.mimetype.toLowerCase() : "";
12
+ if (mimetype.startsWith("video/"))
13
+ return true;
14
+ if (mimetype.startsWith("image/"))
15
+ return false;
16
+ const source = typeof item.url === "string" && item.url
17
+ ? item.url
18
+ : typeof item.path === "string" ? item.path : "";
19
+ return VIDEO_EXTENSION.test(source);
20
+ };
21
+ exports.isAdVideoMedia = isAdVideoMedia;
22
+ const countAdMedia = (media) => {
23
+ const items = Array.isArray(media) ? media : [];
24
+ const videos = items.filter(exports.isAdVideoMedia).length;
25
+ return { images: items.length - videos, videos };
26
+ };
27
+ exports.countAdMedia = countAdMedia;
@@ -1,2 +1,3 @@
1
1
  export * from './converter';
2
2
  export * from './routes';
3
+ export * from './adMedia';
@@ -16,3 +16,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./converter"), exports);
18
18
  __exportStar(require("./routes"), exports);
19
+ __exportStar(require("./adMedia"), exports);
@@ -0,0 +1,13 @@
1
+ // Media is optional on every ad. These are the only limits, shared by every
2
+ // client and the API so they can never drift apart.
3
+ export const MAX_AD_IMAGES = 30;
4
+ export const MAX_AD_VIDEOS = 1;
5
+ export const MAX_AD_VIDEO_BYTES = 50 * 1024 * 1024;
6
+ // Stable codes carried in `params.code` of adMediaSchema issues, so clients can
7
+ // show localized messages without re-implementing the rules.
8
+ export var AdMediaIssueCode;
9
+ (function (AdMediaIssueCode) {
10
+ AdMediaIssueCode["TOO_MANY_IMAGES"] = "too_many_images";
11
+ AdMediaIssueCode["TOO_MANY_VIDEOS"] = "too_many_videos";
12
+ AdMediaIssueCode["VIDEO_TOO_LARGE"] = "video_too_large";
13
+ })(AdMediaIssueCode || (AdMediaIssueCode = {}));
@@ -1 +1,2 @@
1
1
  export * from "./Ad.js";
2
+ export * from "./AdMedia.js";
@@ -1,4 +1,6 @@
1
1
  import { z } from "zod";
2
+ import { AdMediaIssueCode, MAX_AD_IMAGES, MAX_AD_VIDEO_BYTES, MAX_AD_VIDEOS, } from "../../constants/ad/AdMedia.js";
3
+ import { countAdMedia, isAdVideoMedia } from "../../utils/adMedia.js";
2
4
  export const subjectTypeSchema = z.enum([
3
5
  "properties",
4
6
  "services",
@@ -127,6 +129,40 @@ const createServiceSchema = baseServiceSchema;
127
129
  const updateServiceSchema = baseServiceSchema.extend({
128
130
  id: z.coerce.number().int().positive().optional(),
129
131
  });
132
+ // Media is optional: an ad may have no media, only one video, or only images.
133
+ export const adMediaSchema = z
134
+ .array(z.any())
135
+ .superRefine((media, ctx) => {
136
+ const { images, videos } = countAdMedia(media);
137
+ if (images > MAX_AD_IMAGES) {
138
+ ctx.addIssue({
139
+ code: "custom",
140
+ message: `An ad can contain up to ${MAX_AD_IMAGES} images`,
141
+ params: { code: AdMediaIssueCode.TOO_MANY_IMAGES, max: MAX_AD_IMAGES },
142
+ });
143
+ }
144
+ if (videos > MAX_AD_VIDEOS) {
145
+ ctx.addIssue({
146
+ code: "custom",
147
+ message: `An ad can contain up to ${MAX_AD_VIDEOS} video`,
148
+ params: { code: AdMediaIssueCode.TOO_MANY_VIDEOS, max: MAX_AD_VIDEOS },
149
+ });
150
+ }
151
+ media.forEach((item, index) => {
152
+ if (isAdVideoMedia(item) &&
153
+ typeof (item === null || item === void 0 ? void 0 : item.size) === "number" &&
154
+ item.size > MAX_AD_VIDEO_BYTES) {
155
+ ctx.addIssue({
156
+ code: "custom",
157
+ path: [index],
158
+ message: `An ad video cannot exceed ${MAX_AD_VIDEO_BYTES / (1024 * 1024)} MB`,
159
+ params: { code: AdMediaIssueCode.VIDEO_TOO_LARGE, max: MAX_AD_VIDEO_BYTES },
160
+ });
161
+ }
162
+ });
163
+ })
164
+ .optional()
165
+ .nullable();
130
166
  const baseAdCommon = z.object({
131
167
  title: z.string().trim().optional(),
132
168
  status_id: z.coerce.number().int().positive().optional(),
@@ -143,7 +179,7 @@ const baseAdCommon = z.object({
143
179
  return undefined;
144
180
  return value;
145
181
  }, z.coerce.number().optional().nullable()),
146
- media: z.array(z.any()).optional().nullable(),
182
+ media: adMediaSchema,
147
183
  subject_type: subjectTypeSchema.optional(),
148
184
  base_price: z.coerce.number().optional().nullable(),
149
185
  contact_methods: z
@@ -0,0 +1,22 @@
1
+ const VIDEO_EXTENSION = /\.(mp4|mov|webm|m4v|ogv)(?:[?#]|$)/i;
2
+ // Older records can omit mimetype: fall back to the URL extension, and count
3
+ // anything unknown as an image rather than accepting unlimited untyped media.
4
+ export const isAdVideoMedia = (media) => {
5
+ if (!media || typeof media !== "object")
6
+ return false;
7
+ const item = media;
8
+ const mimetype = typeof item.mimetype === "string" ? item.mimetype.toLowerCase() : "";
9
+ if (mimetype.startsWith("video/"))
10
+ return true;
11
+ if (mimetype.startsWith("image/"))
12
+ return false;
13
+ const source = typeof item.url === "string" && item.url
14
+ ? item.url
15
+ : typeof item.path === "string" ? item.path : "";
16
+ return VIDEO_EXTENSION.test(source);
17
+ };
18
+ export const countAdMedia = (media) => {
19
+ const items = Array.isArray(media) ? media : [];
20
+ const videos = items.filter(isAdVideoMedia).length;
21
+ return { images: items.length - videos, videos };
22
+ };
@@ -1,2 +1,3 @@
1
1
  export * from './converter.js';
2
2
  export * from './routes.js';
3
+ export * from './adMedia.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deallony/shared",
3
- "version": "1.2.51",
3
+ "version": "1.2.52",
4
4
  "private": false,
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",